commit 0265f6b91cac3d03debb2ea7c2b5429cc89bcf8b Author: Lukas Endigo Date: Tue Sep 3 09:12:12 2024 +0500 first commit diff --git a/facerecognition/CHANGELOG.md b/facerecognition/CHANGELOG.md new file mode 100644 index 0000000..82fa3b9 --- /dev/null +++ b/facerecognition/CHANGELOG.md @@ -0,0 +1,446 @@ +# Changelog +All notable changes to this project will be documented in this file. + +## [0.9.51] - 2024-06-18 +- Fix admin page when for any reason disable pdlib. +- Adds batch option for face clustering. Issue #712 (Among many others) +- Shows how many processed images there are in the stats command. +- Add setting to enable facial recognition for all users by default. + +## [0.9.50] - 2024-05-22 +- Enable Nextcloud 29 and drop 27. +- Add support to authorization imaginary with key. PR #746, Issue #700. Thanks + to fabalexsie. +- Add button to "Review ignored people". PR #747, Issue #735. Thanks to wronny. +- Add the sixth model to the application. Aka DlibTaguchiHog model. =) + This model was trained from scratch by Taguchi Tokuji to slightly improve the + bias of the original model on non-Caucasian/American people, training with a + greater number of Japanese and others Asians people. It obtained a similar + result in the LFW tests, slightly lower, but within the acceptable margins of + error. + This model should improve the behavior of the application with people with + these traits. +- Update translations. Many thanks to everyone!. + +## [0.9.40] - 2024-04-24 +- Enable PHP 8.3 and NC28 +- Add special modes to background_job command that allows images to be analyzed + in multiple processes to improve speed. +- Add special mode to sync-album command to generate combined albums with multiple + people. PR #709 + +## [0.9.31] - 2023-08-24 +- Be sure to open the model before getting relevant info. Issue #423 + +## [0.9.30] - 2023-08-23 +- Implement the Chinese Whispers Clustering algorithm in native PHP. +- Open the model before requesting information. Issue #679 +- If Imaginary is configured, check that it is accessible before using it. +- If Memories is installed, show people's photos in this app. +- Add face thumbnail when search persons. +- Disable auto rotate for HEIF images in imaginary. Issue #662 +- Add the option to print the progress in json format. + +## [0.9.20] - 2023-06-14 +- Add support for (Now old) Nextcloud 26. +- Add support to NC27 for early testing. +- Clean some code an split great classed to improve maintenance. +- Don't catch Imaginary exceptions. Issue #658 +- Update french translation thanks to Jérémie Tarot. + +## [0.9.12] - 2023-03-25 +- Add support for using imaginary to create the temporary files. + This add support for images heic, tiff, and many more. Issue #494, + #215 and #348 among many other reports. +- Memory optimization in face clustering task. Part of issue #339 + In my tests, it reduces between 33% and 39% of memory, and as an + additional improvement, there was also a reduction in time of around + 19%. There are still several improvements to be made, but it is a + good step. +- Modernizes the construction of the javascript code. Issue #613 +- Fix Unhandled exception and Albums are not being created. Issue #634 + +## [0.9.11] - 2022-12-28 +- Fix migrations on PostgreSQL. Issue #619 and #615 +- Fix OCS Api (API V1). Thanks to nkming2 + +## [0.9.10] - 2022-12-12 +- Just bump version, to remove beta label and allow installing in NC25 +- **Gratitude**: @pulsejet, very kindly accept the integration of this + application into your super cool photo gallery called [Memories](https://github.com/pulsejet/memories). + If you didn't know about this project, I invite you to give it a try, + that will pleasantly surprise you. :tada: + Thanks again. :smiley: +- New Russian translation thanks to Regardo. + +## [0.9.10-beta.2] - 2022-11-17 +### Added +- Adds an API version 2 that theoretically is enough for any client. +- Note that can change minimally until we release the stable version. + +### Fixed +- Only fix tests. + +### Changed +- Use x, y, width, height to save face detections on database. + +## [0.9.10-beta.1] - 2022-09-27 +### Added +- Support Nextcloud 25 +- A little love to the whole application to improve styles and texts. +- Show the image viewer when click come image on our "gallery". + Is press Control+Click it will open the file as before. +- Don´t allow run two face:commands simultaneously to prevent errors. +- Some optimizations on several queries of main view. +- Add a new command to create photos albums of persons. + +### Fixed +- Rephrase I'm not sure button to better indicate what it does. Issue #544 + +### Changed +- Change Person to people since Persons in a very formal word of lawyers. +- Edit people's names in the same side tab instead of using dialogs. + This is really forced by changes to the viewer which retains focus, and + avoids typing anywhere. + It also has a regression that misses the autocomplete. :disapointed: +- Typo fix. Just use plurals on stats table. +- Show the faces of the latest photos, and sort photos by upload order. + +### Translations +- Update German translation thanks to lollos78. +- Update Italian (Italy) translation thanks to lollos78. +- Update of many other translations. Thank you so much everyone. + +## [0.9.5] - 2022-05-07 +### Added +- Just enable Nextcloud 24 + +### Fixed +- Fix NotFoundException on NC24. Issue #574 + +## [0.9.1] - 2021-12-15 +### Fixed +- Fix Ignore persons feature. Issue #542 + +### Translations +- Update Czech translation thanks to Pavel Borecki. +- Update Italian translation thanks to axl84. + +## [0.9.0] - 2021-12-13 +### Added +- Add an extra step to setup. You must indicate exactly how much memory you want + to assing for image processing. See occ face:setup --memory doc on readme. +- Adds the option to effectively ignore persons when assigning names. See issue + #486 #504. +- It also allows you to hide persons that you have already named. Issue #405 +- Implement the option of: This is not such a person. Issue #506 and part of + #158 +- Enable NC23 + +### Translations +- Update some translations. Thank you so much everyone. + +## [0.8.5] - 2021-11-20 +### Added +- Initial support for php 8. See issue #456 +- Add link to show photos of person on sidebar. +- Add static analysis, phpunit and lintian test using github workflow. +- Add an real OCS public API to get all persons. See PR #512. + +### Fixed +- Fix sidebar view when user has disable it. +- Set the Image Area slider to the maximum allowed by the model. See issue #527 +- Don't try to force the setCreationTime argument to be DateTime. See PR #526 +- Migrate hooks to OCP event listeners. See PR #511 + +### Translations +- New Czech translation thanks to Pavel Borecki, and update others. Thank you so + much everyone. + +## [0.8.3] - 2021-07-08 +### Added +- Initial support for NC22. +- Update translations. + +## [0.8.2] - 2021-05-17 +### Added +- Add links in thumbnails of rename persons dialogs. Issue #396 +- Initial autocomplete feature for names. Issue #306 + +### Fixed +- Respect .noimage file, since it is also used in Photos. Issue #446 +- Fix delete files due some change on ORM with NC21. Issue #471 +- Some fixes on make clean. + +### Translations +- New Italian translation thanks to axl84, and update others. Thank you so much + everyone. + +## [0.8.1] - 2021-03-18 +### Fixed +- Register the Hooks within the Bootstrap mechanism, removing many undesirable + logs. Similar to https://github.com/nextcloud/server/issues/22590 + +### Translations +- New Korean (Korea) translation thanks to HyeongJong Choi +- Updating many others translations from Transifex. This time I cannot + individualize your changes to thank you properly, but thank you very much to + all the translators. + +## [0.8.0] - 2021-03-17 +### Added +- Increase the supported version only to NC21. Thanks to @szaimen. See issue #429 +- Add support for unified search, being able to search the photos of your loved + ones from anywhere in nextcloud. Thanks to @dassio. See PR #344 +- Add defer-clustering option. It changes the order of execution of the process + deferring the face clustering at the end of the analysis to get persons in a + simple execution of the command. Thanks to @cliffalbert. See issue #371 + +## [0.7.2] - 2020-12-10 +### Added +- Add an external model that allows run the photos analysis outside of your + Nextcoud instance freeing server resources. This is generic and just define a + reference api, and we share a reference example equivalent to model 1. See + issue #210, #238, and PR #389. + See: https://github.com/matiasdelellis/facerecognition-external-model +- Allow setting a custom model path, useful for configurations like Object + Storage as Primary Storage. This thanks to David Kang. See #381 and #390. +- Add memory info and pdlib version to admin page. PR #385 + +### Translations +- Some messages improved thanks to Robin @Derkades. They were not translated yet + and will probably change again. Please be patient. + +## [0.7.1] - 2020-11-17 +### Added +- Add support for analyzing photos from group folders. Issue #364 + +### Fixed +- Fixed Prevent error when pretty url is disabled on subfolder #379 +- Fix responses when you want to see another user's faces. Issue #352 +- Fix enconde name to URL params queries. Issue #359 +- Typo in title. Issue #357 and #380. Thanks to @strugee +- Try to improve some messages. + +### Translations +- Add Dutch translation thanks to Robin Slot. +- Update Macedonian language thanks to Сашко Тодоров +- Update French language thanks to Tho Vi +- Update Serbian language thanks to Branko Kokanovic. +- Update Spanish language thanks to Matias De lellis. + +## [0.7.0] - 2020-10-24 +### Added +- Support to Nextcloud 20. Issue #343 and #347. Thanks to xiangbin li. +- Add a dialog to assign names to the new persons found. + +### Changed +- The main view of face clusters change switches to a view of persons. We + consider person to the set of all faces with the same name, regardless of the + clusters. Fix issue #334 and parts of #134. +- The second viev show all photos of a person. The server thumbnails are reused, + and therefore the performance is drastically improved. In general fix issue + #193 and parts of #134. +- Finally, the third view, allows you to see all the clusters as before, only to + fix name errors if necessary. + +### Fixed +- Fix crashed on postWrite and prevent other apps to work. Issue #341 + +### Translations +- Update French tranlation thanks to Tho Vi + +## [0.6.3] - 2020-08-28 +### Changed +- Reduce the minimum system memory to 1GB. Issue #319 and others. + +### Fixed +- Fix migration command due 'Invalid datetime format'. Issue #320 +- Fix can't change model to 4, migration says model <4> not installed. Issue #318 + +### Translations +- Update French tranlation thanks to Tho Vi + +## [0.6.2] - 2020-08-14 +### Added +- Introduduce a new model (Model 4, aka DlibCnnHog5) that is 2 times slower, but + much more accurate, which now is in testing stages, and we invite you to test + since probably will be the next recommended model. See PR #313 for details. +- Add face:migrate command that allows to migrate the faces obtained in a model + to a new one. Still recommended to fully analyze the images when changing + models, but can save a lot of time migrating them. See PR #309 +- Add face:reset --model command to just reset current model. + +### Changed +- At least 1000 faces are needed for to make an initial clustering. +- Don't group faces smaller than 40 pixels, which are supposed to be of poor + quality. This is configurable within an advanced hidden setting. PR #299 +- All reset commands require a confirmation to work. +- Hint the 4x3 relation when model recommending memory values. + +## Deprecated +- After many analysis, we discourage the use of model 2 (Aka DlibCnn68). We + still recommend model 1, and model 3 for low-resource devices. You can migrate + the faces using the new command, but we recommend analyzing them again. + +### Fixed +- Fix estimated time in the administration panel. See RP #297 +- Fix that removing .nomedia file does not trigger facerecognition when next + analysis starts. Issue #304 +- Fix travis tests and lot of scrutinizer reports. +- Fix that if increase the minimum confidence dont cluster any face in model 3 +- Log the system info before return any error. Part of issue #278 + +### Translations +- Add Macedonian translation thanks to Сашко Тодоров + +## [0.6.1] - 2020-06-27 +### Changed +- Adjust the appstore makefile rule to ignore vue and teplates files. + +### Bug fixes +- Fix dump models table when none was installed yet. Issue #276. +- Fix integer overflow on 32 bit systems.. Issue #278. +- Fix Admin page when not model installed. Issue #284. + +### Translations +- Update German (Germany) translations thanks to ProfDrJones. + +## [0.6.0] - 2020-05-04 +### Added +- Experimental support of External Storage. Issue #212 +- Add some documentation about how expect to install, test and use the application +- Optionally you can install Pdlib 1.0.2 from https://github.com/matiasdelellis/pdlib that increase the speed of clustering drastically. +- The sidebar has been rewritten to show it in the Photos application. +- Add php-bzip2 as a necessary dependency to install the models. + +### Changed +- Also adds a minimum php memory requirement for HOG. 128 MB. +- Indicate which model is enabled in the summary table of the setup --model command. + +### Bug fixes +- Consider the memory configured in php as a dependency on the models. +- Improves the description of errors and prints the links to documentation whenever it can. +- Fix plurals in admin panel. Issue #256. +- Use paginated queries when search persons on file app. Fix part of issue #263. +- Search mainly faces, rather than persons in file view. Fix issue #264. +- Don't compare a good face with a bad one. This greatly improves the quality of the faces clusters. +- Fix that sidebar says no faces were found when just not clustered yet. Issue #255. +- Fix tests on Nextcloud 18 and 19. + +### Translations +- New French tranlation thanks to Florian Carpentier +- Update Chinese translation thanks to yui874397188 and Jack Frost +- Update German translation thanks to Johannes Szeibert +- Update Polish translation thanks to Piotr Esse +- Update Spanish translation thanks to Matias De lellis + +## [0.5.14] - 2020-03-30 +### Bug fixes +- Fix image will be skipped due 'Unable to open file: /tmp/oc_tmp_###'. Issue #242 + +## [0.5.13] - 2020-03-29 +### Added +- Add face:reset --all|--clustering command. Details in README.md file. +- Add face:stats command. Details in README.md file. +- Add face:progress command. Details in README.md file. +- Multiple model support was implemented. Details on Wiki. +- Allow to change the model using the face:setup command. +- Allow configure the supported mimetype. Details on Wiki. + +### Changed +- Change to use area of the images instead of memory as the main parameter. +- Don't upsampling image on CNN, better pass a bigger picture. +- Don't install any model by default, just dump list of models. +- Better message and print system/php memory as debug. +- Test: Move to new repo that use reprepro and use bionic. + +### Bug fixes +- Fix show not grouped on main view. + +### Translations +- Add Portuguese (Brazil) thanks to Marcelo Rovani. +- Update Chinese language thanks to Jack Frost. +- Update German language thanks to Mark Ziegler. +- Update Spanish language thanks to Matias De lellis. + +## [0.5.12] - 2020-01-06 +### Bug fixes +- Force cast to integer after multiplying by a number. Issue #199, #208 +- Fix initial view when enable analysis and still don't analyze anything. Issue #225 and others. +- face-preview: Person photo is blank #226 + +### Changed +- Change the minimum memory requirement to 2GB. If you have less use Swap. +- Change the minimum memory assigned for analysis to 1.2GB. +- Change the maximum memory assigned for analysis to 8 GB. +- Change the formula to calculate the area according to memory. Issue $220, #176, and others. +- Implement a settings service where to handle everything a little more clean. +- Move FaceManagementService together with the others services. +- Test node binary needed to build handlebars templates. Issue #223 and #217 +- Fix some grammatical errors and typos. Issue 224 + +### Translations +- Add German translation thanks to Mark Ziegler. +- Update Chinese language thanks to Jack Frost. +- Update Spanish language thanks to Matias De lellis. + +## [0.5.11] - 2019-12-20 +### Added +- Add custom exclusion folder option beyond the .nomedia file. Issue #171 +- Add sidebar to folders which allows to enable/disable these. +- Add support for encrypted storage. Issue #201 +- Add experimental support for shared storage. Issue #26 +- Add support to Nextcloud 18 + +### Changed +- Fix travis CI test. +- General cleaning of a lot of code and doc. + +### Translations +- Update Chinese language thanks to Jack Frost. +- Update Spanish language thanks to Matias De lellis. + +## [0.5.10] - 2019-12-06 +### Added +- Add a button to show all clusters with same person name. +- Add a button to go back and show all clusters. + +### Changed +- Select the name when open the rename dialog. +- Remove the spaces at both ends of the names before saving them. +- Force delete invalid entries when a file does not exist. Issue #154 +- Improve tests broken since adding the face:setup command. +- Move from deprecated database.xml to use DB migrations. +- Try to be consistent in that we work with clusters at least in api/endpoints. +- Fix most of the 'App is not compliant' reports. Issue #72 +- Remove Nextcloud 15 support. If you need it, ask for help. + +### Translations +- Update Chinese language thanks to Jack Frost. +- Update Serbian language thanks to Branko Kokanovic. +- Update Spanish language thanks to Matias De lellis. + +## [0.5.9] - 2019-11-25 +### Changed +- Migrate deprecated json_array db type to json. + +## [0.5.8] - 2019-11-22 +### Added +- A personal settings panel to enable the analysis by each user. +- Progressive discovery of faces in the photos of the users. +- Automatic clustering of similar faces as persons. +- Can view and rename face groups in the personal settings pane. +- A side panel where you can see the persons in a photo and rename them. +- Can search for all the photos where a person appears just by typing the name. +- A admin settings panel to configure the main options. + +### Translations +- Added Chinese language thanks to Jack Frost. +- Added Polish language thanks to Olaf Lipinski. +- Added Serbian language thanks to Branko Kokanovic. +- Added Spanish language thanks to Matias De lellis. + +## [0.5.4] – 2017-10-04 + +- Initial release diff --git a/facerecognition/ISSUE_TEMPLATE.md b/facerecognition/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..620e027 --- /dev/null +++ b/facerecognition/ISSUE_TEMPLATE.md @@ -0,0 +1,82 @@ +Hey, Thanks for reporting issues back to Nextcloud Face Recognition. Please, try to complete this report in detail so we can help you easier. :smile: + +Make sure you read all the [documentation](https://github.com/matiasdelellis/facerecognition/wiki), and the [FAQ](https://github.com/matiasdelellis/facerecognition/wiki/FAQ), and that the issue has not been reported before. :wink: + +### Expected behaviour + +Tell us what should happen + +### Actual behaviour + +Tell us what happens instead + +### Steps to reproduce +1. +2. +3. + +### Server configuration + +* **Operating system:** + +* **Pdlib version:** + +* **How is DLib installed:** Make sure it is working correctly with this [tool](https://github.com/matiasdelellis/pdlib-min-test-suite) + +* **How is PDlib installed:** Make sure it is working correctly with this [tool](https://github.com/matiasdelellis/pdlib-min-test-suite) + +* **PHP version:** + +* **Web server:** + +* **Database:** + +* **Nextcloud version:** + +### Client configuration + +* **Browser:** + +* **Operating system:** + +### Logs + +#### Background task log with debug. +
+sudo -u apache php occ -vvv face:background_job + +``` +Insert your background log here +``` +
+ +#### Web server error log +
+Web server error log + +``` +Insert your webserver log here +``` +
+ +#### Nextcloud log (data/nextcloud.log) +
+Nextcloud log + +``` +Insert your Nextcloud log here +``` +
+ +#### Browser log +
+Browser log + +``` +Insert your browser log here, this could for example include: + +a) The javascript console log +b) The network log +c) ... +``` +
diff --git a/facerecognition/LICENSE b/facerecognition/LICENSE new file mode 100644 index 0000000..dbbe355 --- /dev/null +++ b/facerecognition/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/facerecognition/README.md b/facerecognition/README.md new file mode 100644 index 0000000..a5a6f0a --- /dev/null +++ b/facerecognition/README.md @@ -0,0 +1,213 @@ +# Face Recognition + +![PHPUnit Status](https://img.shields.io/github/workflow/status/matiasdelellis/facerecognition/PHPUnit) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/matiasdelellis/facerecognition/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/matiasdelellis/facerecognition/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/matiasdelellis/facerecognition/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/matiasdelellis/facerecognition/?branch=master) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/4b035bd1283349009ad88235d37ddae1)](https://www.codacy.com/app/stalker314314/facerecognition?utm_source=github.com&utm_medium=referral&utm_content=matiasdelellis/facerecognition&utm_campaign=Badge_Grade) +![Downloads](https://img.shields.io/github/downloads/matiasdelellis/facerecognition/total) +[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html) + +Nextcloud app that implement a basic facial recognition system. + +FaceRecognition is a Nextcloud application with a goal of recognizing, analyzing +and aggregating face data in users images, and providing additional +functionalities on top of these information, all with built-in privacy of +Nextcloud. Imagine Google Photos, but only for faces (not detecting objects…) +and in such way that your images never leave your Nextcloud instance. :smiley: + +The application listens to the creation of new image files, and queues them for +later analysis. A scheduled task (Or admin on demand) take this queue, and +analyze the images for looking faces and if possible identify them grouping by +similarity. + +![App screenshots](https://matiasdelellis.github.io/img/facerecognition/facerecognition-persons-view-small.jpeg "App screenshots") + +## How to use it? + +The administrator must properly configure the application, and once it is +working, the user must accept that he wants to allow the analysis of his images +to discover his friends. +Finally the user can use the application in three ways + + 1. In the user settings there is a 'Face Recognition' panel where first of all + each user must enable the analysis. Once enabled, you will progressively see + the discovery of your friends, and you can assign them names. + 2. In the file application the user can search by typing your friend's name, + and it will show all the photos. + 3. In the side panel of the file application, a 'Persons' tab is added where + you can see a list of your friends in the photo, and rename them. Also you can + select the folders you want to ignore for the process. + 3. In the side panel of the Photos application, a 'Persons' tab is added where + you can see a list of your friends in the photo, and rename them. + +## Donate + +If you'd like to support the creation and maintenance of this software, consider donating. + +[![Donate](https://img.shields.io/badge/Donate-PayPal-blue)](https://github.com/matiasdelellis/facerecognition/wiki/Donate) +[![Donate](https://img.shields.io/badge/Donate-Bitcoin-orange)](https://github.com/matiasdelellis/facerecognition/wiki/Donate) +[![Donate](https://img.shields.io/badge/Donate-Ethereum-blueviolet)](https://github.com/matiasdelellis/facerecognition/wiki/Donate) + +## Installation, configuration and usage + +#### Requirements + + * Nextcloud 22+ + * [Dlib PHP bindings](https://github.com/goodspb/pdlib) + * [PHP Bzip2](https://www.php.net/manual/en/book.bzip2.php) + * 1GB of RAM + +#### Installation + +Ideally once you meet the requirements, you can install and enable it from the +nextcloud app store. For details and advanced information read the documentation +about [installation](https://github.com/matiasdelellis/facerecognition/wiki/Installation). + +#### Configuration + +Before proceeding to analyze the images, you must indicate how much memory you +want to assigns to image processing and then must properly install and configure +the pretrained models using the `occ face:setup` command. For details and +advanced information read the documentation about [models](https://github.com/matiasdelellis/facerecognition/wiki/Models#install-models). + +Then you must indicate the size of the images used in the temporary files from +the Nextcloud settings panel. This configuration will depend on your +installation and has a direct impact on memory consumption. For details and +advanced information read the documentation about [Temporary files](https://github.com/matiasdelellis/facerecognition/wiki/Settings#temporary-files). + +#### Test the application + +We recommend test the application intensively before proceeding to analyze the +real data of the users. For this you can create a new user in your Nextcloud +instance and upload some photos from the internet. Then you must run the +`occ face:background_job -u new_user -t 900` command for this user and evaluate +the result. For details and advanced information read the documentation of this +command below. + +#### Schedule background job + +The application is designed to run as a scheduled task. This allows analyze the +photos and showing the results to the user progressively. You can read about +some ways to configure it within our documentation about [Schedule Background Task](https://github.com/matiasdelellis/facerecognition/wiki/Schedule-Background-Task). + +## occ commands + +The application add commands to the [Nexcloud's command-line interface](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html). + +#### Initial setup + +`occ face:setup [-M|--memory MEMORY] [-m|--model MODEL]` + +This command is responsible for making the necessary settings to use the +application. + +If `MEMORY` is supplied, it will establish the maximum memory to be used for the +processing of the images. This value will be limited according to the memory +available by the system and the php configuration. You can use numbers as bytes +(1073741824 for 1GB), or subfixed with units (1024M or 1G) but note that it is +without space. + +If `MODEL_ID` is supplied. the pre-trained model for facial recognition will be +installed. + +You must perform both settings before continuing with any application command. +If you do not supply any of these options, the command will return the current +configuration. + +#### Face analysis + +`occ face:background_job [-u|--user_id USER_ID] [-t|--timeout TIMEOUT] [--defer-clustering] [-M|--max_image_area MAX_IMAGE_AREA]` + +This command will do all the work. It is responsible for searching the images, +analyzing them and clustering faces found in them in groups of similar people. + +Beware that this command can take a lot of CPU and memory! Before you put it to +cron job, it is advised to try it out manually first, just to be sure you have +all requirements and you have enough resources on your machine. + +Command is designed to be run continuously, so you will want to schedule it with +cron to be executed every once in a while, together with a specified timeout. It +can be run every 15 minutes with timeout of `-t 900` (so, it will stop itself +automatically after 15 minutes and cron will start it again), or once a day with +timeout of 2 hours, like `-t 7200`. + +If `USER_ID` is supplied, it will just loop over files of a given user. Keep in +mind that each user must enable the analysis individually, and otherwise this +command will ignore the user. + +If `TIMEOUT` is supplied it will stop after the indicated seconds, and continue +in the next execution. Use this value in conjunction with the times of the +scheduled task to distribute the system load during the day. + +If `MAX_IMAGE_AREA` is supplied caps the maximum area (in pixels^2) of the image +to be fed to neural network, effectively lowering needed memory. Use this +if face detection crashes randomly. + +If use the `--defer-clustering` option, it changes the order of execution of the +process deferring the face clustering at the end of the analysis to get persons +in a simple execution of the command. + +#### Create albums in the Photos app + +`face:sync-albums [-u|--user_id USER_ID]` + +This command creates photo albums within the Nexcloud Photos app, with photos of +each person found. + +Note that these albums are editable in the Photos app, and any changes will be +ignored and reverted on the next run of this command. + +This command is also designed to be run regularly to sync any user changes, as +this command is the only one that will update albums. + +If `USER_ID` is provided, it will just sync albums for this user. + +#### Resetting information + +`occ face:reset [--all] [--model] [--image-errors] [--clustering] [-u|--user_id USER_ID]` + +This command can completely wipe out all images, faces and cluster of persons. +It is ideal if you want to start from scratch for any reason. + +You must specify if you wish to completely reset the database `[--all]` or just +the current model `[--model]` and all images must be analyzed again, or or you +can reset only the clustering of persons `[--clustering]` and only clustering +needs to be done again, or reset only the images that had errors +`[--image-errors]` to try to analyze them again. + +If `USER_ID` is provided, it will just reset the information of a particular +user. + +#### Migrate models + +`occ face:migrate [-m|--model_id MODEL_ID] [-u|--user_id USER_ID]` + +This command allows to migrate the faces obtained in a model to a new one. Note +that the persons name are not migrated, and the user must rename them again. +Always is recommended to analyze from scratch any configured model, but you can +save a lot of time migrating it. + +You must specify which model you want to migrate using the `MODEL_ID` option. + +If `USER_ID` is provided, just migrate the faces for the given user. + +#### Statistics + +`occ face:stats [-u|--user_id USER_ID] [-j|--json]` + +This command return a summary of the number of images, faces and persons found. + +If `USER_ID` is provided, just return the stats for the given user. + +If use the `--json` argument, it prints the stats in a json format more suitable +to parse with other tools. + +#### Progress + +`occ face:progress` + +This command just return the progress of the analysis and an estimated time to +complete. + +If use the `--json` argument, it prints the stats in a json format more suitable +to parse with other tools. diff --git a/facerecognition/TESTING.md b/facerecognition/TESTING.md new file mode 100644 index 0000000..bfd33eb --- /dev/null +++ b/facerecognition/TESTING.md @@ -0,0 +1,33 @@ +# Testing + +## Running tests + +All tests should be easily executed with simple: +```bash +make test +``` +from root directory. However, you will not be able to execute integration tests out of the box, as they are modifying local instance. For those, you will have to explicitely export env variable: +```bash +export TRAVIS=1 +``` +to be able to run them. Again – make sure you are running test server and watch out as those tests can create havoc with your instance. + +Once you have all requirements satisified and you want to run test over and over again, it all boils down to calling: +```bash +phpunit -c phpunit.xml +``` +from root directory. You can target specific tests with `--filter` etc., but this is all *regular* PHP unit testing, just type `phpunit -h` for more details. + +If you are missing PHPUnit, you can follow their [official guide](https://phpunit.de/getting-started/phpunit-7.html), but you should get binary with somwething like this: + +```bash +wget -O phpunit https://phar.phpunit.de/phpunit-7.phar +chmod +x phpunit +./phpunit --version +``` + +## Writing tests + +As by standard definition, unit tests are not modifying external resources and should not touch database. If possible, you should try testing with unit tests, and if it not – add yourself to integration tests. + +Other than that, you are good to go – there are already some unit and integration tests in `tests/` directory, and you can always take a look at [Nextcloud unit testing official documentation](https://docs.nextcloud.com/server/14/developer_manual/core/unit-testing.html). \ No newline at end of file diff --git a/facerecognition/appinfo/info.xml b/facerecognition/appinfo/info.xml new file mode 100644 index 0000000..d23a58d --- /dev/null +++ b/facerecognition/appinfo/info.xml @@ -0,0 +1,59 @@ + + + facerecognition + Face Recognition + A face recognition app + + + 0.9.51 + agpl + Matias De lellis + Branko Kokanovic + FaceRecognition + + + + multimedia + https://github.com/matiasdelellis/facerecognition + https://github.com/matiasdelellis/facerecognition/issues + https://github.com/matiasdelellis/facerecognition.git + https://matiasdelellis.github.io/img/facerecognition/facerecognition-persons-view.jpeg + https://matiasdelellis.github.io/img/facerecognition/facerecognition-person-photos.jpeg + https://matiasdelellis.github.io/img/facerecognition/facerecognition-photos-integration.jpeg + https://matiasdelellis.github.io/img/facerecognition/facerecognition-assign-initial-name.jpeg + + + + + + + OCA\FaceRecognition\Migration\RemoveFullImageScanDoneFlag + + + + OCA\FaceRecognition\Command\BackgroundCommand + OCA\FaceRecognition\Command\MigrateCommand + OCA\FaceRecognition\Command\ProgressCommand + OCA\FaceRecognition\Command\ResetCommand + OCA\FaceRecognition\Command\SetupCommand + OCA\FaceRecognition\Command\StatsCommand + OCA\FaceRecognition\Command\SyncAlbumsCommand + + + OCA\FaceRecognition\Settings\Admin + OCA\FaceRecognition\Settings\AdminSection + OCA\FaceRecognition\Settings\Personal + OCA\FaceRecognition\Settings\PersonalSection + + diff --git a/facerecognition/appinfo/routes.php b/facerecognition/appinfo/routes.php new file mode 100644 index 0000000..d9b4985 --- /dev/null +++ b/facerecognition/appinfo/routes.php @@ -0,0 +1,217 @@ + +[ + /* + * Persons + */ + // Get all face clusters with faces and file asociated. + [ + 'name' => 'person#index', + 'url' => '/persons', + 'verb' => 'GET' + ], + // Get all images filtered by Name. + [ + 'name' => 'person#find', + 'url' => '/person/{personName}', + 'verb' => 'GET' + ], + // Change name to a person. + [ + 'name' => 'person#updateName', + 'url' => '/person/{personName}', + 'verb' => 'PUT' + ], + // Change visibility to a person. + [ + 'name' => 'person#setVisibility', + 'url' => '/person/{personName}/visibility', + 'verb' => 'POST' + ], + // Get all names filtered by an query. + [ + 'name' => 'person#autocomplete', + 'url' => '/autocomplete/{query}', + 'verb' => 'GET' + ], + /* + * Clusters + */ + // Get a cluster by Id. + [ + 'name' => 'cluster#find', + 'url' => '/cluster/{id}', + 'verb' => 'GET' + ], + // Get all clusters filtered by Name. + [ + 'name' => 'cluster#findByName', + 'url' => '/clusters/{personName}', + 'verb' => 'GET' + ], + // Get all clusters unassigned clusters. + [ + 'name' => 'cluster#findUnassigned', + 'url' => '/clusters', + 'verb' => 'GET' + ], + // Get all clusters ignored clusters. + [ + 'name' => 'cluster#findIgnored', + 'url' => '/clustersIgnored', + 'verb' => 'GET' + ], + // Change visibility to cluster + [ + 'name' => 'cluster#setVisibility', + 'url' => '/cluster/{id}/visibility', + 'verb' => 'POST' + ], + // Detach Face from cluster + [ + 'name' => 'cluster#detachFace', + 'url' => '/cluster/{id}/detach', + 'verb' => 'PUT' + ], + // Change name to a cluster. + [ + 'name' => 'cluster#updateName', + 'url' => '/cluster/{id}', + 'verb' => 'PUT' + ], + /* + * Face thumbails + */ + // Get a face Thumb + [ + 'name' => 'face#getThumb', + 'url' => '/face/{id}/thumb/{size}', + 'verb' => 'GET' + ], + // Get a face Thumb of some person + [ + 'name' => 'face#getPersonThumb', + 'url' => '/person/{name}/thumb/{size}', + 'verb' => 'GET' + ], + /* + * File and Folders + */ + // Get persons from path + [ + 'name' => 'file#getPersonsFromPath', + 'url' => '/file', + 'verb' => 'GET' + ], + // Get folder preferences + [ + 'name' => 'file#getFolderOptions', + 'url' => '/folder', + 'verb' => 'GET' + ], + // Set folder preferences + [ + 'name' => 'file#setFolderOptions', + 'url' => '/folder', + 'verb' => 'PUT' + ], + /* + * Settings + */ + // User settings + [ + 'name' => 'settings#setUserValue', + 'url' => '/setuservalue', + 'verb' => 'POST' + ], + [ + 'name' => 'settings#getUserValue', + 'url' => '/getuservalue', + 'verb' => 'GET' + ], + // App settings + [ + 'name' => 'settings#setAppValue', + 'url' => '/setappvalue', + 'verb' => 'POST' + ], + [ + 'name' => 'settings#getAppValue', + 'url' => '/getappvalue', + 'verb' => 'GET' + ], + /* + * Status of process. + */ + // Get process status. + [ + 'name' => 'process#index', + 'url' => '/process', + 'verb' => 'GET' + ], + + /* + * Face Recognition API V2 + */ + // Get all named persons + [ + 'name' => 'api#getPersonsV2', + 'url' => '/api/2.0/persons', + 'verb' => 'GET', + ], + // Get all photos associated to a person + [ + 'name' => 'api#getPerson', + 'url' => '/api/2.0/person/{personName}', + 'verb' => 'GET', + ], + // Change name to a person or hide it + [ + 'name' => 'Api#updatePerson', + 'url' => '/api/2.0/person/{personName}', + 'verb' => 'PUT' + ], + // Change name to a cluster or hide it. + [ + 'name' => 'Api#updateCluster', + 'url' => '/api/2.0/cluster/{clusterId}', + 'verb' => 'PUT' + ], + // Get all names filtered by an query. + [ + 'name' => 'Api#autocomplete', + 'url' => '/api/2.0/autocomplete', + 'verb' => 'GET' + ], + // Get all unassigned clusters to name + [ + 'name' => 'Api#discoverPerson', + 'url' => '/api/2.0/discover', + 'verb' => 'GET' + ], + // Detach Face from cluster + [ + 'name' => 'Api#detachFace', + 'url' => '/api/2.0/face/{faceId}/detach', + 'verb' => 'PUT' + ], + +], 'ocs' => [ + + /* + * OCS Person API V1 + */ + // Get all named persons + [ + 'name' => 'ocs_api#getPersonsV1', + 'url' => '/api/v1/persons', + 'verb' => 'GET', + ], + // Get all faces associated to a person + [ + 'name' => 'ocs_api#getFacesByPerson', + 'url' => '/api/v1/person/{name}/faces', + 'verb' => 'GET', + ], + +]]; diff --git a/facerecognition/css/facerecognition.css b/facerecognition/css/facerecognition.css new file mode 100644 index 0000000..7cbb1c5 --- /dev/null +++ b/facerecognition/css/facerecognition.css @@ -0,0 +1,121 @@ +#div-content { + width: 100%; +} + +#optional-buttons-div { + margin-top: 22px; +} + +.icon-back { + background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmlld2JveD0iMCAwIDE2IDE2IgogICB3aWR0aD0iMTYiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIxNiIKICAgaWQ9InN2ZzQwIgogICBzb2RpcG9kaTpkb2NuYW1lPSJjb25maXJtLnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wLjEgKDNiYzJlODEzZjUsIDIwMjAtMDktMDcpIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE0NiI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczQ0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMSIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDI1IgogICAgIGlkPSJuYW1lZHZpZXc0MiIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iNTEuODc1IgogICAgIGlua3NjYXBlOmN4PSI4IgogICAgIGlua3NjYXBlOmN5PSI4IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIwIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNDAiIC8+CiAgPHBhdGgKICAgICBkPSJtIDcuNDk4MzI2NSwxNS41MjQxNyBjIDAuODk3NCwwIDEuMzQwNCwtMS4wOTA5IDAuNjk3MywtMS43MTY4IGwgLTQuNzgzNywtNC43ODMxOTk4IEggMTQuOTg0OTI3IGMgMS4zNTIzLDAuMDE5MTI1IDEuMzUyMywtMi4wMTkxIDAsLTIgSCAzLjQxMjkyNjUgbCA0Ljc4MzIsLTQuNzgzMTk5NiBjIDAuOTgxNjMsLTAuOTQyNTEgLTAuNDcxNTUsLTIuMzk1NzAwMDQgLTEuNDE0MSwtMS40MTQxMDAwNCBMIDAuMjkwOTI2NDgsNy4zMTY4NzAyIGMgLTAuMzg3LDAuMzg3OCAtMC4zOTEsMS4wMjI4IDAsMS40MTQgTCA2Ljc4MTUyNjUsMTUuMjIxMTcgYyAwLjE4ODMsMC4xOTM1IDAuNDQ2OCwwLjMwMjY4IDAuNzE2OCwwLjMwMjcgeiIKICAgICBpZD0icGF0aDM4IiAvPgo8L3N2Zz4K") +} + +.image-box, +.person-box { + display: inline-block; + margin: 3px; +} + +.person-desc { + text-align: center; + margin-top: -5px; + color: var(--color-text-lighter); +} + +.cluster-title { + padding: 10px; + text-align: left; + margin-top: 22px; + margin-bottom: 0px !important; +} + +.title-name { + padding-left: 12px; + padding-right: 12px; + padding-top: 2px; +} + +.title-icon { + width: 36px; + height: 36px; + border-radius: 18px;; +} + +.title-icon:hover { + opacity: 1; + background-color: rgba(127,127,127,.25) !important; +} +.person-name { + text-align: center; + margin-top: 5px; + margin-bottom: 0px; +} + +.persons-previews { + margin-top: 22px; +} + +.faces-previews { + margin-left: 24px; + display: flex; + flex-wrap: wrap; +} + +.face-preview-big { + background-color: rgba(210, 210, 210, .75); + background-size: cover; + border-radius: 50%; + height: 128px; + width: 128px; + margin: 0 auto; + cursor: pointer; +} + +.file-preview-big { + background-color: rgba(210, 210, 210, .75); + background-size: cover; + border-radius: 12px; + height: 128px; + width: 128px; + margin: 0 auto; + cursor: pointer; +} + +.face-preview { + background-color: rgba(210, 210, 210, .75); + background-size: cover; + border-radius: 3px; + margin: 2px; + height: 50px; + width: 50px; +} + +.face-preview-big:hover, +.face-preview:hover { + opacity: .5; +} + +/* + * Admin page + */ +.icon-align { + padding: 11px 20px; +} + +.ui-slider { + vertical-align: middle; + width: 40%; +} + +.settings-ranged > label:first-child { + display: inline-block; + width: 25%; + text-align: right; +} + +.span-highlighted { + text-align: center; + padding: 2px 5px; + border-radius: 10px; + background-color: darkgray; +} diff --git a/facerecognition/css/facerecognition.scss b/facerecognition/css/facerecognition.scss new file mode 100644 index 0000000..3070b0a --- /dev/null +++ b/facerecognition/css/facerecognition.scss @@ -0,0 +1 @@ +// Fake sass to fix test until fix webpack.. \ No newline at end of file diff --git a/facerecognition/css/fr-dialogs.css b/facerecognition/css/fr-dialogs.css new file mode 100644 index 0000000..fa9869e --- /dev/null +++ b/facerecognition/css/fr-dialogs.css @@ -0,0 +1,51 @@ +/* Dialog */ + +#fr-assign-dialog-input, +#fr-rename-dialog-input, +#fr-detach-face-dialog-input { + width: 100%; +} + +.face-preview-dialog { + background-color: rgba(210, 210, 210, .75); + border-radius: 50%; + height: 50px; + width: 50px; + margin: 1px; +} + +/* Autocomplete */ + +ul.autocomplete { + width: 90%; + display: none; + border: 1px solid #999; + border-radius: 3px; + background: #fff; + padding-left: 0; + list-style: none; + overflow: auto; + z-index: 1; +} + +ul.autocomplete li { + padding: .3em 1em; + white-space: nowrap; + cursor: default; + color: var(--color-text-lighter); +} + +ul.autocomplete li.selected { + background: var(--color-background-dark); + color: var(--color-main-text); +} + +ul.autocomplete li.no-results { + text-align: center; +} + +ul.autocomplete mark { + color: inherit; + background: none; + font-weight: 700; +} diff --git a/facerecognition/img/app-dark.svg b/facerecognition/img/app-dark.svg new file mode 100644 index 0000000..97361be --- /dev/null +++ b/facerecognition/img/app-dark.svg @@ -0,0 +1 @@ + diff --git a/facerecognition/img/app.svg b/facerecognition/img/app.svg new file mode 100644 index 0000000..b3cf71b --- /dev/null +++ b/facerecognition/img/app.svg @@ -0,0 +1,4 @@ + + + + diff --git a/facerecognition/img/back.svg b/facerecognition/img/back.svg new file mode 100644 index 0000000..6107932 --- /dev/null +++ b/facerecognition/img/back.svg @@ -0,0 +1,53 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/facerecognition/img/glasses.png b/facerecognition/img/glasses.png new file mode 100644 index 0000000..13a9431 Binary files /dev/null and b/facerecognition/img/glasses.png differ diff --git a/facerecognition/img/mustache.png b/facerecognition/img/mustache.png new file mode 100644 index 0000000..75973e9 Binary files /dev/null and b/facerecognition/img/mustache.png differ diff --git a/facerecognition/js/facerecognition-admin.js b/facerecognition/js/facerecognition-admin.js new file mode 100644 index 0000000..0a6e206 --- /dev/null +++ b/facerecognition/js/facerecognition-admin.js @@ -0,0 +1,2 @@ +(()=>{"use strict";$(document).ready((function(){const e=0,a=2;function i(){$.get(OC.generateUrl("/apps/facerecognition/process")).done((function(e){if(e.status){var a="";if(e.processedImages==e.totalImages)a=t("facerecognition","The analysis is finished"),a+=" - ",a+=n("facerecognition","%n image was analyzed","%n images were analyzed",e.totalImages);else{var i=e.totalImages-e.processedImages,s=Date.now()+1e3*e.estimatedFinalize;a=t("facerecognition","Analyzing images"),a+=" - ",a+=n("facerecognition","%n image detected","%n images detected",e.totalImages),a+=" - ",a+=n("facerecognition","%n image in queue","%n images in queue",i),a+=" - ",a+=t("facerecognition","Ends approximately {estimatedFinalize}",{estimatedFinalize:OC.Util.relativeModifiedDate(s)})}$("#progress-text").html(a),$("#progress-bar").attr("value",e.processedImages),$("#progress-bar").attr("max",e.totalImages)}else{$("#progress-bar").attr("value",0);a=t("facerecognition","The analysis is not started yet");a+=" - ",a+=n("facerecognition","%n image in queue","%n images in queue",e.totalImages),$("#progress-text").html(a)}}))}function s(){$.ajax({type:"GET",url:OC.generateUrl("apps/facerecognition/getappvalue"),data:{type:"analysis_image_area"},success:function(a){if(a.status===e){var n=parseInt(a.value);$("#image-area-range").val(n),$("#image-area-value").html(l(n))}}})}function o(){$.ajax({type:"GET",url:OC.generateUrl("apps/facerecognition/getappvalue"),data:{type:"sensitivity"},success:function(a){if(a.status===e){var n=parseFloat(a.value);$("#sensitivity-range").val(n),$("#sensitivity-value").html(n)}}})}function r(){$.ajax({type:"GET",url:OC.generateUrl("apps/facerecognition/getappvalue"),data:{type:"min_confidence"},success:function(a){if(a.status===e){var n=parseFloat(a.value);$("#min-confidence-range").val(n),$("#min-confidence-value").html(n)}}})}function c(){$.ajax({type:"GET",url:OC.generateUrl("apps/facerecognition/getappvalue"),data:{type:"min_faces_in_cluster"},success:function(a){if(a.status===e){var n=parseInt(a.value);$("#min-no-faces-range").val(n),$("#min-no-faces-value").html(n)}}})}function l(e){var a=Math.sqrt(4*e/3),n=3*a/4;return Math.floor(a)+"x"+Math.floor(n)+" (4x3)"}$("#image-area-range").on("input",(function(){$("#image-area-value").html(l(this.value)),$("#restore-image-area").show(),$("#save-image-area").show()})),$("#restore-image-area").on("click",(function(e){e.preventDefault(),s(),$("#restore-image-area").hide(),$("#save-image-area").hide()})),$("#save-image-area").on("click",(function(e){e.preventDefault();var n=$("#image-area-range").val().toString();$.ajax({type:"POST",url:OC.generateUrl("apps/facerecognition/setappvalue"),data:{type:"analysis_image_area",value:n},success:function(e){if(e.status===a)OC.Notification.showTemporary(t("facerecognition","The changes were saved. It will be taken into account in the next analysis.")),$("#restore-image-area").hide(),$("#save-image-area").hide();else{var n=parseInt(e.value);$("#image-area-range").val(n),$("#image-area-value").html(l(n));var i=t("facerecognition","The change could not be applied.");i+=" - "+e.message,OC.Notification.showTemporary(i)}}})})),$("#sensitivity-range").on("input",(function(){$("#sensitivity-value").html(this.value),$("#restore-sensitivity").show(),$("#save-sensitivity").show()})),$("#restore-sensitivity").on("click",(function(e){e.preventDefault(),o(),$("#restore-sensitivity").hide(),$("#save-sensitivity").hide()})),$("#save-sensitivity").on("click",(function(e){e.preventDefault();var n=$("#sensitivity-range").val().toString();$.ajax({type:"POST",url:OC.generateUrl("apps/facerecognition/setappvalue"),data:{type:"sensitivity",value:n},success:function(e){e.status===a&&(OC.Notification.showTemporary(t("facerecognition","The changes were saved. It will be taken into account in the next analysis.")),$("#restore-sensitivity").hide(),$("#save-sensitivity").hide())}})})),$("#min-confidence-range").on("input",(function(){$("#min-confidence-value").html(this.value),$("#restore-min-confidence").show(),$("#save-min-confidence").show()})),$("#restore-min-confidence").on("click",(function(e){e.preventDefault(),r(),$("#restore-min-confidence").hide(),$("#save-min-confidence").hide()})),$("#save-min-confidence").on("click",(function(e){e.preventDefault();var n=$("#min-confidence-range").val().toString();$.ajax({type:"POST",url:OC.generateUrl("apps/facerecognition/setappvalue"),data:{type:"min_confidence",value:n},success:function(e){e.status===a&&(OC.Notification.showTemporary(t("facerecognition","The changes were saved. It will be taken into account in the next analysis.")),$("#restore-min-confidence").hide(),$("#save-min-confidence").hide())}})})),$("#min-no-faces-range").on("input",(function(){$("#min-no-faces-value").html(this.value),$("#restore-min-no-faces").show(),$("#save-min-no-faces").show()})),$("#restore-min-no-faces").on("click",(function(e){e.preventDefault(),c(),$("#restore-min-no-faces").hide(),$("#save-min-no-faces").hide()})),$("#save-min-no-faces").on("click",(function(e){e.preventDefault();var n=$("#min-no-faces-range").val().toString();$.ajax({type:"POST",url:OC.generateUrl("apps/facerecognition/setappvalue"),data:{type:"min_faces_in_cluster",value:n},success:function(e){e.status===a&&(OC.Notification.showTemporary(t("facerecognition","Done")),$("#restore-min-no-faces").hide(),$("#save-min-no-faces").hide())}})})),s(),o(),r(),c(),i(),window.setInterval(i,5e3)}))})(); +//# sourceMappingURL=facerecognition-admin.js.map?v=7c6e1b844b240ad8d11a \ No newline at end of file diff --git a/facerecognition/js/facerecognition-dialogs.js b/facerecognition/js/facerecognition-dialogs.js new file mode 100644 index 0000000..da7b7b5 --- /dev/null +++ b/facerecognition/js/facerecognition-dialogs.js @@ -0,0 +1,486 @@ +/* + * @copyright 2019-2021 Matias De lellis + * + * @author 2019 Matias De lellis + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * this class to ease the usage of jquery dialogs + */ +const FrDialogs = { + + hide: function (faces, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-hidee-dialog'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'Hide person'), + message: t('facerecognition', 'You can still see that person in the photos, but assigning a name will only be for that photo.'), + type: 'none' + }); + + $dlg.append($('
')); + + var div = $('
').attr('style', 'text-align: center'); + $dlg.append(div); + + for (var face of faces) { + if (face['fileUrl'] !== undefined) { + div.append($('')); + } else { + div.append($('')); + } + } + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Cancel'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(false); + } + } + }, { + text: t('facerecognition', 'Hide'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true); + } + }, + defaultButton: true + }]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false); + } + } + }); + }); + }, + + rename: function (name, faces, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-rename-dialog'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'Rename person'), + message: t('facerecognition', 'Please enter a name to rename the person'), + type: 'none' + }); + + $dlg.append($('
')); + + var div = $('
').attr('style', 'text-align: center'); + $dlg.append(div); + + for (var face of faces) { + if (face['fileUrl'] !== undefined) { + div.append($('')); + } else { + div.append($('')); + } + } + + var input = $('').attr('type', 'text').attr('id', dialogName + '-input').attr('placeholder', name).attr('value', name); + $dlg.append(input); + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Cancel'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(false, input.val().trim()); + } + } + }, { + text: t('facerecognition', 'Rename'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, input.val().trim()); + } + }, + defaultButton: true + }]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false, input.val()); + } + } + }); + + new AutoComplete({ + input: document.getElementById(dialogName + "-input"), + lookup (query) { + return new Promise(resolve => { + $.get(OC.generateUrl('/apps/facerecognition/autocomplete/' + query)).done(function (names) { + resolve(names); + }); + }); + }, + silent: true, + highlight: false + }); + + $(dialogId + "-input").keydown(function(event) { + // It only prevents the that change the image when you press arrow keys. + event.stopPropagation(); + if (event.key === "Enter") { + // It only prevents the that change the image when you press enter. + event.preventDefault(); + } + }); + + input.focus(); + input.select(); + }); + }, + + detachFace: function (face, oldName, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-detach-face-dialog'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'This person is not {name}', {name: oldName}), + message: t('facerecognition', 'Optionally you can assign the correct name'), + type: 'none' + }); + + $dlg.append($('
')); + + var div = $('
').attr('style', 'text-align: center'); + $dlg.append(div); + + div.append($('')); + + var input = $('').attr('type', 'text').attr('id', dialogName + '-input').attr('placeholder', t('facerecognition', 'Please assign a name to this person.')); + $dlg.append(input); + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Cancel'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(false, null); + } + }, + }, { + text: t('facerecognition', 'Save'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, input.val().trim().length > 0 ? input.val().trim() : null); + } + }, + defaultButton: true + }]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false, null); + } + } + }); + + new AutoComplete({ + input: document.getElementById(dialogName + "-input"), + lookup (query) { + return new Promise(resolve => { + $.get(OC.generateUrl('/apps/facerecognition/autocomplete/' + query)).done(function (names) { + resolve(names); + }); + }); + }, + silent: true, + highlight: false + }); + + $(dialogId + "-input").keydown(function(event) { + // It only prevents the that change the image when you press arrow keys. + event.stopPropagation(); + if (event.key === "Enter") { + // It only prevents the that change the image when you press enter. + event.preventDefault(); + } + }); + + input.focus(); + }); + }, + + assignName: function (faces, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-assign-dialog'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'Add name'), + message: t('facerecognition', 'Please assign a name to this person.'), + type: 'none' + }); + + $dlg.append($('
')); + + var div = $('
').attr('style', 'text-align: center'); + $dlg.append(div); + + for (var face of faces) { + if (face['fileUrl'] !== undefined) { + div.append($('')); + } else { + div.append($('')); + } + } + + var input = $('').attr('type', 'text').attr('id', dialogName + '-input').attr('placeholder', t('facerecognition', 'Please assign a name to this person.')); + $dlg.append(input); + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Ignore'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, null); + } + }, + }, { + text: t('facerecognition', 'Skip for now'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, ''); + } + }, + defaultButton: false + }, { + text: t('facerecognition', 'Save'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, input.val().trim()); + } + }, + defaultButton: true + }]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false, ''); + } + } + }); + + new AutoComplete({ + input: document.getElementById(dialogName + "-input"), + lookup (query) { + return new Promise(resolve => { + $.get(OC.generateUrl('/apps/facerecognition/autocomplete/' + query)).done(function (names) { + resolve(names); + }); + }); + }, + silent: true, + highlight: false + }); + + $(dialogId + "-input").keydown(function(event) { + // It only prevents the that change the image when you press arrow keys. + event.stopPropagation(); + if (event.key === "Enter") { + // It only prevents the that change the image when you press enter. + event.preventDefault(); + } + }); + + input.focus(); + }); + }, + + assignIgnored: function (faces, callback) { + return $.when(this._getMessageTemplate()).then(function ($tmpl) { + var dialogName = 'fr-assign-dialog'; + var dialogId = '#' + dialogName; + var $dlg = $tmpl.octemplate({ + dialog_name: dialogName, + title: t('facerecognition', 'Add name'), + message: t('facerecognition', 'Please assign a name to this person.'), + type: 'none' + }); + + $dlg.append($('
')); + + var div = $('
').attr('style', 'text-align: center'); + $dlg.append(div); + + for (var face of faces) { + if (face['fileUrl'] !== undefined) { + div.append($('')); + } else { + div.append($('')); + } + } + + var input = $('').attr('type', 'text').attr('id', dialogName + '-input').attr('placeholder', t('facerecognition', 'Please assign a name to this person.')); + $dlg.append(input); + + $('body').append($dlg); + + // wrap callback in _.once(): + // only call callback once and not twice (button handler and close + // event) but call it for the close event, if ESC or the x is hit + if (callback !== undefined) { + callback = _.once(callback); + } + + var buttonlist = [{ + text: t('facerecognition', 'Keep ignored'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, null); + } + }, + }, { + text: t('facerecognition', 'Save'), + click: function () { + $(dialogId).ocdialog('close'); + if (callback !== undefined) { + callback(true, input.val().trim()); + } + }, + defaultButton: true + }]; + + $(dialogId).ocdialog({ + closeOnEscape: true, + modal: true, + buttons: buttonlist, + close: function () { + // callback is already fired if Yes/No is clicked directly + if (callback !== undefined) { + callback(false, ''); + } + } + }); + + new AutoComplete({ + input: document.getElementById(dialogName + "-input"), + lookup (query) { + return new Promise(resolve => { + $.get(OC.generateUrl('/apps/facerecognition/autocomplete/' + query)).done(function (names) { + resolve(names); + }); + }); + }, + silent: true, + highlight: false + }); + + $(dialogId + "-input").keydown(function(event) { + // It only prevents the that change the image when you press arrow keys. + event.stopPropagation(); + if (event.key === "Enter") { + // It only prevents the that change the image when you press enter. + event.preventDefault(); + } + }); + + input.focus(); + }); + }, + + _getMessageTemplate: function () { + var defer = $.Deferred(); + if (!this.$messageTemplate) { + var self = this; + $.get(OC.filePath('core', 'templates', 'message.html'), function (tmpl) { + self.$messageTemplate = $(tmpl); + defer.resolve(self.$messageTemplate); + }) + .fail(function (jqXHR, textStatus, errorThrown) { + defer.reject(jqXHR.status, errorThrown); + }); + } else { + defer.resolve(this.$messageTemplate); + } + return defer.promise(); + } + +} diff --git a/facerecognition/js/facerecognition-personal.js b/facerecognition/js/facerecognition-personal.js new file mode 100644 index 0000000..f1ff407 --- /dev/null +++ b/facerecognition/js/facerecognition-personal.js @@ -0,0 +1,2 @@ +!function(e,o,i,r){"use strict";i(document).ready((function(){var s=function(e){this._baseUrl=e,this._persons=[],this._activePerson=r,this._clustersByName=[],this._unassignedClusters=[],this._ignoredClusters=[],this._loaded=!1,this._mustReload=!1};s.prototype={isLoaded:function(){return this._loaded},mustReload:function(){return this._mustReload},load:function(){var e=i.Deferred(),n=this;return i.get(this._baseUrl+"/persons").done((function(o){n._persons=o.persons.sort((function(e,n){return n.count-e.count})),n._loaded=!0,n._mustReload=!1,e.resolve()})).fail((function(){e.reject()})),e.promise()},loadPerson:function(e){this.unsetActive();var n=i.Deferred(),o=this;return i.get(this._baseUrl+"/person/"+encodeURIComponent(e)).done((function(e){o._activePerson=e,n.resolve()})).fail((function(){n.reject()})),n.promise()},getPersons:function(){return this._persons},getActivePerson:function(){return this._activePerson},renamePerson:function(e,n){var o=this,t=i.Deferred(),r={name:n};return i.ajax({url:this._baseUrl+"/person/"+encodeURIComponent(e),method:"PUT",contentType:"application/json",data:JSON.stringify(r)}).done((function(e){o._activePerson=e,o._mustReload=!0,t.resolve()})).fail((function(){t.reject()})),t.promise()},setVisibility:function(e,n){var o=this,t=i.Deferred(),r={visible:n};return i.ajax({url:this._baseUrl+"/person/"+encodeURIComponent(e)+"/visibility",method:"POST",contentType:"application/json",data:JSON.stringify(r)}).done((function(e){o._mustReload=!0,t.resolve()})).fail((function(){t.reject()})),t.promise()},loadClustersByName:function(e){var n=i.Deferred(),o=this;return i.get(this._baseUrl+"/clusters/"+encodeURIComponent(e)).done((function(e){o._clustersByName=e.clusters.sort((function(e,n){return n.count-e.count})),n.resolve()})).fail((function(){n.reject()})),n.promise()},loadUnassignedClusters:function(){this._unassignedClusters=[];var e=i.Deferred(),n=this;return i.get(this._baseUrl+"/clusters").done((function(o){n._unassignedClusters=o.clusters.sort((function(e,n){return n.count-e.count})),e.resolve()})).fail((function(){e.reject()})),e.promise()},loadIgnoredClusters:function(){this._ignoredClusters=[];var e=i.Deferred(),n=this;return i.get(this._baseUrl+"/clustersIgnored").done((function(o){n._ignoredClusters=o.clusters.sort((function(e,n){return n.count-e.count})),e.resolve()})).fail((function(){e.reject()})),e.promise()},getClustersByName:function(){return this._clustersByName},getUnassignedClusters:function(){return this._unassignedClusters},getIgnoredClusters:function(){return this._ignoredClusters},getNamedClusterById:function(e){var n=r;for(var o of this._clustersByName)if(o.id===e){n=o;break}return n},renameCluster:function(e,n){var o=this,t=i.Deferred(),r={name:n};return i.ajax({url:this._baseUrl+"/cluster/"+e,method:"PUT",contentType:"application/json",data:JSON.stringify(r)}).done((function(i){o._clustersByName.forEach((function(o){o.id===e&&(o.name=n)})),o._mustReload=!0,t.resolve()})).fail((function(){t.reject()})),t.promise()},setClusterVisibility:function(e,n){var o=this,t=i.Deferred(),r={visible:n};return i.ajax({url:this._baseUrl+"/cluster/"+e+"/visibility",method:"POST",contentType:"application/json",data:JSON.stringify(r)}).done((function(n){var i=o._clustersByName.findIndex((n=>n.id===e));o._clustersByName.splice(i,1),o._mustReload=!0,t.resolve()})).fail((function(){t.reject()})),t.promise()},unsetActive:function(){this._activePerson=r,this._clustersByName=[]}};var a=function(e){this._enabled=OCP.InitialState.loadState("facerecognition","user-enabled"),this._hasUnamed=OCP.InitialState.loadState("facerecognition","has-unamed"),this._hasHidden=OCP.InitialState.loadState("facerecognition","has-hidden"),this._persons=e,this._observer=lozad(".lozad")};a.prototype={reload:function(n){var o=this;this._persons.load().done((function(){o.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error trying to show your friends"))}))},setEnabledUser:function(n){var o=this;i.ajax({type:"POST",url:e.generateUrl("apps/facerecognition/setuservalue"),data:{type:"enabled",value:n},success:function(){n?e.Notification.showTemporary(t("facerecognition","The analysis is enabled, please be patient, you will soon see your friends here.")):e.Notification.showTemporary(t("facerecognition","The analysis is disabled. Soon all the information found for facial recognition will be removed.")),o._enabled=n,o.reload()}})},renameUnassignedClusterDialog:function(){var n=this,o=this._persons.getUnassignedClusters().shift();if(o===r)return e.Notification.showTemporary(t("facerecognition","You don't have more people to recognize.")),n.renderContent(),void(n._persons.mustReload()&&n.reload());FrDialogs.assignName(o.faces,(function(i,r){!0===i?null!==r?r.length>0?n._persons.renameCluster(o.id,r).done((function(){n.renameUnassignedClusterDialog()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error renaming this person"))})):n.renameUnassignedClusterDialog():n._persons.setClusterVisibility(o.id,!1).done((function(){n.renameUnassignedClusterDialog()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error ignoring this person"))})):n._persons.mustReload()&&n.reload()}))},renameIgnoredClusterDialog:function(){var n=this,o=this._persons.getIgnoredClusters().shift();if(o===r)return e.Notification.showTemporary(t("facerecognition","You no longer have people ignored")),n.renderContent(),void(n._persons.mustReload()&&n.reload());FrDialogs.assignIgnored(o.faces,(function(i,r){!0===i?null!==r&&r.length>0?n._persons.renameCluster(o.id,r).done((function(){n.renameIgnoredClusterDialog()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error renaming this person"))})):n.renameIgnoredClusterDialog():n._persons.mustReload()&&n.reload()}))},renderContent:function(){var n={loaded:this._persons.isLoaded(),appName:t("facerecognition","Face Recognition"),welcomeHint:t("facerecognition","Here you can see photos of your friends that are recognized"),enableDescription:t("facerecognition","Analyze my images and group my loved ones with similar faces"),loadingMsg:t("facerecognition","Looking for your recognized friends"),showMoreButton:t("facerecognition","Review face groups"),showIgnoredButton:t("facerecognition","Review ignored people"),emptyMsg:t("facerecognition","The analysis is disabled"),emptyHint:t("facerecognition","Enable it to find your loved ones"),renameHint:t("facerecognition","Rename"),hideHint:t("facerecognition","Hide it"),loadingIcon:e.imagePath("core","loading.gif")};!0===this._enabled&&(n.enabled=!0,n.hasUnamed=this._hasUnamed,n.hasHidden=this._hasHidden,n.persons=this._persons.getPersons(),n.reviewPeopleMsg=t("facerecognition","Review people found"),n.reviewIgnoredMsg=t("facerecognition","Review ignored people"),n.emptyMsg=t("facerecognition","Your friends have not been recognized yet"),n.emptyHint=t("facerecognition","Please, be patient"));var s=this._persons.getActivePerson();s!=r&&(n.personName=s.name,n.personImages=s.images);var a=this._persons.getClustersByName();a.length>0&&(n.clustersByName=a);var l=Handlebars.templates.personal(n);i("#div-content").html(l),this._observer.observe(),s!==r?c(s.name):c();var u=this;i("#enableFacerecognition").click((function(){!1===i(this).is(":checked")?e.dialogs.confirm(t("facerecognition","You will lose all the information analyzed, and if you re-enable it, you will start from scratch."),t("facerecognition","Do you want to deactivate the grouping by faces?"),(function(e){!0===e?u.setEnabledUser(!1):i("#enableFacerecognition").prop("checked",!0)}),!0):u.setEnabledUser(!0)})),i("#show-more-clusters").click((function(){u._persons.loadUnassignedClusters().done((function(){u._persons.getUnassignedClusters().length>0?u.renameUnassignedClusterDialog():e.Notification.showTemporary(t("facerecognition","You dont have more people to recognize."))}))})),i("#show-ignored-clusters").click((function(){u._persons.loadIgnoredClusters().done((function(){u._persons.getIgnoredClusters().length>0?u.renameIgnoredClusterDialog():e.Notification.showTemporary(t("facerecognition","You no longer have people ignored"))}))})),i("#facerecognition .file-preview-big").click((function(){var e=i(this).data("id");if(o.event.ctrlKey){var n=u._persons.getActivePerson().images.find((function(n){return n.filename==e}));o.open(n.fileUrl,"_blank")}else{var t=u._persons.getActivePerson().images.map((function(e){return{basename:e.basename,filename:e.filename,mime:e.mimetype}}));OCA.Viewer.open({path:e,list:t})}})),i("#facerecognition .face-preview-big").click((function(){i(this).css("cursor","wait");var n=i(this).parent().data("id");u._persons.loadPerson(n).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error when trying to find photos of your friend"))}))})),i("#facerecognition #rename-person").click((function(){var n=u._persons.getActivePerson();FrDialogs.rename(n.name,[n],(function(o,i){!0===o&&i&&u._persons.renamePerson(n.name,i).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error renaming this person"))}))}))})),i("#facerecognition #hide-person").click((function(){var n=u._persons.getActivePerson();FrDialogs.hide([n],(function(o){!0===o&&u._persons.setVisibility(n.name,!1).done((function(){u._persons.unsetActive(),u.reload()})).fail((function(){e.Notification.showTemporary(t("facerecognition","An error occurred while hiding this person"))}))}))})),i("#facerecognition #rename-cluster").click((function(){var n=i(this).data("id"),o=u._persons.getNamedClusterById(n);FrDialogs.rename(o.name,[o.faces[0]],(function(o,i){!0===o&&i&&u._persons.renameCluster(n,i).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error renaming this cluster of faces"))}))}))})),i("#facerecognition #hide-cluster").click((function(){var n=i(this).data("id"),o=u._persons.getNamedClusterById(n);FrDialogs.hide([o.faces[0]],(function(o){!0===o&&u._persons.setClusterVisibility(n,!1).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","An error occurred while hiding this group of faces"))}))}))})),i("#facerecognition #review-person-clusters").click((function(){i(this).css("cursor","wait");var n=u._persons.getActivePerson();u._persons.loadClustersByName(n.name).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error when trying to find photos of your friend"))}))})),i("#facerecognition #show-ignored-clusters").click((function(){i(this).css("cursor","wait");var n=u._persons.getActivePerson();u._persons.loadClustersByName(n.name).done((function(){u.renderContent()})).fail((function(){e.Notification.showTemporary(t("facerecognition","There was an error when trying to find photos of your friend"))}))})),i("#facerecognition .icon-back").click((function(){u._persons.unsetActive(),u.renderContent(),!u._persons.mustReload()&&u._persons.isLoaded()||u.reload()}))}};var c=function(e){var n=o.location.href.split("?")[0],i=t("facerecognition","Face Recognition");e&&(n+="?name="+encodeURIComponent(e),i+=" - "+e),o.history.replaceState({},i,n),document.title=i};Handlebars.registerHelper("noPhotos",(function(e){return n("facerecognition","%n image","%n images",e)}));var l=new s(e.generateUrl("/apps/facerecognition")),u=new a(l),f=function(){var e=r,n=document.createElement("a");n.href=o.location.href;for(var t=n.search.substring(1).split("&"),i=0;i{var e={7737:(e,t,n)=>{const r=n(5503),{MAX_LENGTH:o,MAX_SAFE_INTEGER:a}=n(5519),{re:i,t:s}=n(8238),l=n(4433),{compareIdentifiers:u}=n(3242);class c{constructor(e,t){if(t=l(t),e instanceof c){if(e.loose===!!t.loose&&e.includePrerelease===!!t.includePrerelease)return e;e=e.version}else if("string"!=typeof e)throw new TypeError(`Invalid version. Must be a string. Got type "${typeof e}".`);if(e.length>o)throw new TypeError(`version is longer than ${o} characters`);r("SemVer",e,t),this.options=t,this.loose=!!t.loose,this.includePrerelease=!!t.includePrerelease;const n=e.trim().match(t.loose?i[s.LOOSE]:i[s.FULL]);if(!n)throw new TypeError(`Invalid Version: ${e}`);if(this.raw=e,this.major=+n[1],this.minor=+n[2],this.patch=+n[3],this.major>a||this.major<0)throw new TypeError("Invalid major version");if(this.minor>a||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>a||this.patch<0)throw new TypeError("Invalid patch version");n[4]?this.prerelease=n[4].split(".").map((e=>{if(/^[0-9]+$/.test(e)){const t=+e;if(t>=0&&t=0;)"number"==typeof this.prerelease[r]&&(this.prerelease[r]++,r=-2);if(-1===r){if(t===this.prerelease.join(".")&&!1===n)throw new Error("invalid increment argument: identifier already exists");this.prerelease.push(e)}}if(t){let r=[t,e];!1===n&&(r=[t]),0===u(this.prerelease[0],t)?isNaN(this.prerelease[1])&&(this.prerelease=r):this.prerelease=r}break}default:throw new Error(`invalid increment argument: ${e}`)}return this.format(),this.raw=this.version,this}}e.exports=c},2426:(e,t,n)=>{const r=n(7737);e.exports=(e,t)=>new r(e,t).major},7488:(e,t,n)=>{const r=n(7737);e.exports=(e,t,n=!1)=>{if(e instanceof r)return e;try{return new r(e,t)}catch(e){if(!n)return null;throw e}}},7907:(e,t,n)=>{const r=n(7488);e.exports=(e,t)=>{const n=r(e,t);return n?n.version:null}},5519:e=>{const t=Number.MAX_SAFE_INTEGER||9007199254740991;e.exports={MAX_LENGTH:256,MAX_SAFE_COMPONENT_LENGTH:16,MAX_SAFE_INTEGER:t,RELEASE_TYPES:["major","premajor","minor","preminor","patch","prepatch","prerelease"],SEMVER_SPEC_VERSION:"2.0.0",FLAG_INCLUDE_PRERELEASE:1,FLAG_LOOSE:2}},5503:(e,t,n)=>{var r=n(4155);const o="object"==typeof r&&r.env&&r.env.NODE_DEBUG&&/\bsemver\b/i.test(r.env.NODE_DEBUG)?(...e)=>console.error("SEMVER",...e):()=>{};e.exports=o},3242:e=>{const t=/^[0-9]+$/,n=(e,n)=>{const r=t.test(e),o=t.test(n);return r&&o&&(e=+e,n=+n),e===n?0:r&&!o?-1:o&&!r?1:en(t,e)}},4433:e=>{const t=Object.freeze({loose:!0}),n=Object.freeze({});e.exports=e=>e?"object"!=typeof e?t:e:n},8238:(e,t,n)=>{const{MAX_SAFE_COMPONENT_LENGTH:r}=n(5519),o=n(5503),a=(t=e.exports={}).re=[],i=t.src=[],s=t.t={};let l=0;const u=(e,t,n)=>{const r=l++;o(e,r,t),s[e]=r,i[r]=t,a[r]=new RegExp(t,n?"g":void 0)};u("NUMERICIDENTIFIER","0|[1-9]\\d*"),u("NUMERICIDENTIFIERLOOSE","[0-9]+"),u("NONNUMERICIDENTIFIER","\\d*[a-zA-Z-][a-zA-Z0-9-]*"),u("MAINVERSION",`(${i[s.NUMERICIDENTIFIER]})\\.(${i[s.NUMERICIDENTIFIER]})\\.(${i[s.NUMERICIDENTIFIER]})`),u("MAINVERSIONLOOSE",`(${i[s.NUMERICIDENTIFIERLOOSE]})\\.(${i[s.NUMERICIDENTIFIERLOOSE]})\\.(${i[s.NUMERICIDENTIFIERLOOSE]})`),u("PRERELEASEIDENTIFIER",`(?:${i[s.NUMERICIDENTIFIER]}|${i[s.NONNUMERICIDENTIFIER]})`),u("PRERELEASEIDENTIFIERLOOSE",`(?:${i[s.NUMERICIDENTIFIERLOOSE]}|${i[s.NONNUMERICIDENTIFIER]})`),u("PRERELEASE",`(?:-(${i[s.PRERELEASEIDENTIFIER]}(?:\\.${i[s.PRERELEASEIDENTIFIER]})*))`),u("PRERELEASELOOSE",`(?:-?(${i[s.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${i[s.PRERELEASEIDENTIFIERLOOSE]})*))`),u("BUILDIDENTIFIER","[0-9A-Za-z-]+"),u("BUILD",`(?:\\+(${i[s.BUILDIDENTIFIER]}(?:\\.${i[s.BUILDIDENTIFIER]})*))`),u("FULLPLAIN",`v?${i[s.MAINVERSION]}${i[s.PRERELEASE]}?${i[s.BUILD]}?`),u("FULL",`^${i[s.FULLPLAIN]}$`),u("LOOSEPLAIN",`[v=\\s]*${i[s.MAINVERSIONLOOSE]}${i[s.PRERELEASELOOSE]}?${i[s.BUILD]}?`),u("LOOSE",`^${i[s.LOOSEPLAIN]}$`),u("GTLT","((?:<|>)?=?)"),u("XRANGEIDENTIFIERLOOSE",`${i[s.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`),u("XRANGEIDENTIFIER",`${i[s.NUMERICIDENTIFIER]}|x|X|\\*`),u("XRANGEPLAIN",`[v=\\s]*(${i[s.XRANGEIDENTIFIER]})(?:\\.(${i[s.XRANGEIDENTIFIER]})(?:\\.(${i[s.XRANGEIDENTIFIER]})(?:${i[s.PRERELEASE]})?${i[s.BUILD]}?)?)?`),u("XRANGEPLAINLOOSE",`[v=\\s]*(${i[s.XRANGEIDENTIFIERLOOSE]})(?:\\.(${i[s.XRANGEIDENTIFIERLOOSE]})(?:\\.(${i[s.XRANGEIDENTIFIERLOOSE]})(?:${i[s.PRERELEASELOOSE]})?${i[s.BUILD]}?)?)?`),u("XRANGE",`^${i[s.GTLT]}\\s*${i[s.XRANGEPLAIN]}$`),u("XRANGELOOSE",`^${i[s.GTLT]}\\s*${i[s.XRANGEPLAINLOOSE]}$`),u("COERCE",`(^|[^\\d])(\\d{1,${r}})(?:\\.(\\d{1,${r}}))?(?:\\.(\\d{1,${r}}))?(?:$|[^\\d])`),u("COERCERTL",i[s.COERCE],!0),u("LONETILDE","(?:~>?)"),u("TILDETRIM",`(\\s*)${i[s.LONETILDE]}\\s+`,!0),t.tildeTrimReplace="$1~",u("TILDE",`^${i[s.LONETILDE]}${i[s.XRANGEPLAIN]}$`),u("TILDELOOSE",`^${i[s.LONETILDE]}${i[s.XRANGEPLAINLOOSE]}$`),u("LONECARET","(?:\\^)"),u("CARETTRIM",`(\\s*)${i[s.LONECARET]}\\s+`,!0),t.caretTrimReplace="$1^",u("CARET",`^${i[s.LONECARET]}${i[s.XRANGEPLAIN]}$`),u("CARETLOOSE",`^${i[s.LONECARET]}${i[s.XRANGEPLAINLOOSE]}$`),u("COMPARATORLOOSE",`^${i[s.GTLT]}\\s*(${i[s.LOOSEPLAIN]})$|^$`),u("COMPARATOR",`^${i[s.GTLT]}\\s*(${i[s.FULLPLAIN]})$|^$`),u("COMPARATORTRIM",`(\\s*)${i[s.GTLT]}\\s*(${i[s.LOOSEPLAIN]}|${i[s.XRANGEPLAIN]})`,!0),t.comparatorTrimReplace="$1$2$3",u("HYPHENRANGE",`^\\s*(${i[s.XRANGEPLAIN]})\\s+-\\s+(${i[s.XRANGEPLAIN]})\\s*$`),u("HYPHENRANGELOOSE",`^\\s*(${i[s.XRANGEPLAINLOOSE]})\\s+-\\s+(${i[s.XRANGEPLAINLOOSE]})\\s*$`),u("STAR","(<|>)?=?\\s*\\*"),u("GTE0","^\\s*>=\\s*0\\.0\\.0\\s*$"),u("GTE0PRE","^\\s*>=\\s*0\\.0\\.0-0\\s*$")},9944:(e,t,n)=>{"use strict";function r(){return document.documentElement.dataset.locale||"en"}n(9070),t.Iu=function(e,t,n,r,o){if("undefined"==typeof OC)return console.warn("No OC found"),t;return OC.L10N.translate(e,t,n,r,o)},t.uN=function(e,t,n,r,o,a){if("undefined"==typeof OC)return console.warn("No OC found"),t;return OC.L10N.translatePlural(e,t,n,r,o,a)},n(4916),n(5306)},9753:(e,t,n)=>{"use strict";n(9070),Object.defineProperty(t,"__esModule",{value:!0}),t.linkTo=t.imagePath=t.getRootUrl=t.generateUrl=t.generateRemoteUrl=t.generateOcsUrl=t.generateFilePath=void 0,n(9601),n(4916),n(5306),n(1539),n(9714),n(2772);t.linkTo=function(e,t){return o(e,"",t)};t.generateRemoteUrl=function(e){return window.location.protocol+"//"+window.location.host+function(e){return a()+"/remote.php/"+e}(e)};t.generateOcsUrl=function(e,t,n){var o=1===Object.assign({ocsVersion:2},n||{}).ocsVersion?1:2;return window.location.protocol+"//"+window.location.host+a()+"/ocs/v"+o+".php"+r(e,t,n)};var r=function(e,t,n){var r,o=Object.assign({escape:!0},n||{});return"/"!==e.charAt(0)&&(e="/"+e),r=(r=t||{})||{},e.replace(/{([^{}]*)}/g,(function(e,t){var n=r[t];return o.escape?"string"==typeof n||"number"==typeof n?encodeURIComponent(n.toString()):encodeURIComponent(e):"string"==typeof n||"number"==typeof n?n.toString():e}))};t.generateUrl=function(e,t,n){var o,i,s,l=Object.assign({noRewrite:!1},n||{});return!0!==(null===(o=window)||void 0===o||null===(i=o.OC)||void 0===i||null===(s=i.config)||void 0===s?void 0:s.modRewriteWorking)||l.noRewrite?a()+"/index.php"+r(e,t,n):a()+r(e,t,n)};t.imagePath=function(e,t){return-1===t.indexOf(".")?o(e,"img",t+".svg"):o(e,"img",t)};var o=function(e,t,n){var r,o,i,s=-1!==(null===(r=window)||void 0===r||null===(o=r.OC)||void 0===o||null===(i=o.coreApps)||void 0===i?void 0:i.indexOf(e)),l=a();if("php"!==n.substring(n.length-3)||s)if("php"===n.substring(n.length-3)||s)l+="settings"!==e&&"core"!==e&&"search"!==e||"ajax"!==t?"/":"/index.php/",s||(l+="apps/"),""!==e&&(l+=e+="/"),t&&(l+=t+"/"),l+=n;else{var u,c,p;l=null===(u=window)||void 0===u||null===(c=u.OC)||void 0===c||null===(p=c.appswebroots)||void 0===p?void 0:p[e],t&&(l+="/"+t+"/"),"/"!==l.substring(l.length-1)&&(l+="/"),l+=n}else l+="/index.php/apps/"+e,"index.php"!==n&&(l+="/",t&&(l+=encodeURI(t+"/")),l+=n);return l};t.generateFilePath=o;var a=function(){var e,t;return(null===(e=window)||void 0===e||null===(t=e.OC)||void 0===t?void 0:t.webroot)||""};t.getRootUrl=a},250:(e,t,n)=>{!function(t,n){e.exports=n()}(self,(()=>(()=>{var e={4891:(e,t,n)=>{"use strict";n.d(t,{default:()=>z});var r=n(9104),o=n(5825),a=n(1205),i=n(932),s=n(2734),l=n.n(s),u=n(1441),c=n.n(u);function p(e){return p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},p(e)}function d(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function f(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0])||arguments[0];this.opened&&(this.opened=!1,this.$refs.popover.clearFocusTrap({returnFocus:e}),this.$emit("update:open",!1),this.$emit("close"),this.opened=!1,this.focusIndex=0,this.$refs.menuButton.$el.focus())},onOpen:function(e){var t=this;this.$nextTick((function(){t.focusFirstAction(e)}))},onMouseFocusAction:function(e){if(document.activeElement!==e.target){var t=e.target.closest("li");if(t){var n=t.querySelector(g);if(n){var r=v(this.$refs.menu.querySelectorAll(g)).indexOf(n);r>-1&&(this.focusIndex=r,this.focusAction())}}}},onKeydown:function(e){(38===e.keyCode||9===e.keyCode&&e.shiftKey)&&this.focusPreviousAction(e),(40===e.keyCode||9===e.keyCode&&!e.shiftKey)&&this.focusNextAction(e),33===e.keyCode&&this.focusFirstAction(e),34===e.keyCode&&this.focusLastAction(e),27===e.keyCode&&(this.closeMenu(),e.preventDefault())},removeCurrentActive:function(){var e=this.$refs.menu.querySelector("li.active");e&&e.classList.remove("active")},focusAction:function(){var e=this.$refs.menu.querySelectorAll(g)[this.focusIndex];if(e){this.removeCurrentActive();var t=e.closest("li.action");e.focus(),t&&t.classList.add("active")}},focusPreviousAction:function(e){this.opened&&(0===this.focusIndex?this.closeMenu():(this.preventIfEvent(e),this.focusIndex=this.focusIndex-1),this.focusAction())},focusNextAction:function(e){if(this.opened){var t=this.$refs.menu.querySelectorAll(g).length-1;this.focusIndex===t?this.closeMenu():(this.preventIfEvent(e),this.focusIndex=this.focusIndex+1),this.focusAction()}},focusFirstAction:function(e){this.opened&&(this.preventIfEvent(e),this.focusIndex=0,this.focusAction())},focusLastAction:function(e){this.opened&&(this.preventIfEvent(e),this.focusIndex=this.$refs.menu.querySelectorAll(g).length-1,this.focusAction())},preventIfEvent:function(e){e&&(e.preventDefault(),e.stopPropagation())},onFocus:function(e){this.$emit("focus",e)},onBlur:function(e){this.$emit("blur",e)}},render:function(e){var t=this,n=(this.$slots.default||[]).filter((function(e){var t,n,r,o;return(null==e||null===(t=e.componentOptions)||void 0===t?void 0:t.tag)||(null==e||null===(n=e.componentOptions)||void 0===n||null===(r=n.Ctor)||void 0===r||null===(o=r.extendOptions)||void 0===o?void 0:o.name)})),r=n.filter(this.isValidSingleAction);if(this.forceMenu&&r.length>0&&this.inline>0&&(l().util.warn("Specifying forceMenu will ignore any inline actions rendering."),r=[]),0!==n.length){var o=function(n){var r,o,a,i,s,l,u,c,p,d,m,v,h,g,b,y,A,w,C,x,k,_,S=(null==n||null===(r=n.data)||void 0===r||null===(o=r.scopedSlots)||void 0===o||null===(a=o.icon())||void 0===a?void 0:a[0])||e("span",{class:["icon",null==n||null===(i=n.componentOptions)||void 0===i||null===(s=i.propsData)||void 0===s?void 0:s.icon]}),O=null==n||null===(l=n.componentOptions)||void 0===l||null===(u=l.listeners)||void 0===u?void 0:u.click,E=null==n||null===(c=n.componentOptions)||void 0===c||null===(p=c.children)||void 0===p||null===(d=p[0])||void 0===d||null===(m=d.text)||void 0===m||null===(v=m.trim)||void 0===v?void 0:v.call(m),T=(null==n||null===(h=n.componentOptions)||void 0===h||null===(g=h.propsData)||void 0===g?void 0:g.ariaLabel)||E,P=t.forceTitle?E:"",j=null==n||null===(b=n.componentOptions)||void 0===b||null===(y=b.propsData)||void 0===y?void 0:y.title;return t.forceTitle||j||(j=E),e("NcButton",{class:["action-item action-item--single",null==n||null===(A=n.data)||void 0===A?void 0:A.staticClass,null==n||null===(w=n.data)||void 0===w?void 0:w.class],attrs:{"aria-label":T,title:j},ref:null==n||null===(C=n.data)||void 0===C?void 0:C.ref,props:f({type:t.type||(P?"secondary":"tertiary"),disabled:t.disabled||(null==n||null===(x=n.componentOptions)||void 0===x||null===(k=x.propsData)||void 0===k?void 0:k.disabled),ariaHidden:t.ariaHidden},null==n||null===(_=n.componentOptions)||void 0===_?void 0:_.propsData),on:f({focus:t.onFocus,blur:t.onBlur},!!O&&{click:function(e){O&&O(e)}})},[e("template",{slot:"icon"},[S]),P])},a=function(n){var r,o,a=(null===(r=t.$slots.icon)||void 0===r?void 0:r[0])||(t.defaultIcon?e("span",{class:["icon",t.defaultIcon]}):e("DotsHorizontal",{props:{size:20}}));return e("NcPopover",{ref:"popover",props:{delay:0,handleResize:!0,shown:t.opened,placement:t.placement,boundary:t.boundariesElement,container:t.container,popoverBaseClass:"action-item__popper",setReturnFocus:null===(o=t.$refs.menuButton)||void 0===o?void 0:o.$el},attrs:{delay:0,handleResize:!0,shown:t.opened,placement:t.placement,boundary:t.boundariesElement,container:t.container,popoverBaseClass:"action-item__popper"},on:{show:t.openMenu,"after-show":t.onOpen,hide:t.closeMenu}},[e("NcButton",{class:"action-item__menutoggle",props:{type:t.triggerBtnType,disabled:t.disabled,ariaHidden:t.ariaHidden},slot:"trigger",ref:"menuButton",attrs:{"aria-haspopup":"menu","aria-label":t.ariaLabel,"aria-controls":t.opened?t.randomId:null,"aria-expanded":t.opened.toString()},on:{focus:t.onFocus,blur:t.onBlur}},[e("template",{slot:"icon"},[a]),t.menuTitle]),e("div",{class:{open:t.opened},attrs:{tabindex:"-1"},on:{keydown:t.onKeydown,mousemove:t.onMouseFocusAction},ref:"menu"},[e("ul",{attrs:{id:t.randomId,tabindex:"-1",role:"menu"}},[n])])])};if(1===n.length&&1===r.length&&!this.forceMenu)return o(r[0]);if(r.length>0&&this.inline>0){var i=r.slice(0,this.inline),s=n.filter((function(e){return!i.includes(e)}));return e("div",{class:["action-items","action-item--".concat(this.triggerBtnType)]},[].concat(v(i.map(o)),[s.length>0?e("div",{class:["action-item",{"action-item--open":this.opened}]},[a(s)]):null]))}return e("div",{class:["action-item action-item--default-popover","action-item--".concat(this.triggerBtnType),{"action-item--open":this.opened}]},[a(n)])}}};var y=n(3379),A=n.n(y),w=n(7795),C=n.n(w),x=n(569),k=n.n(x),_=n(3565),S=n.n(_),O=n(9216),E=n.n(O),T=n(4589),P=n.n(T),j=n(9286),N={};N.styleTagTransform=P(),N.setAttributes=S(),N.insert=k().bind(null,"head"),N.domAPI=C(),N.insertStyleElement=E(),A()(j.Z,N),j.Z&&j.Z.locals&&j.Z.locals;var D=n(2377),$={};$.styleTagTransform=P(),$.setAttributes=S(),$.insert=k().bind(null,"head"),$.domAPI=C(),$.insertStyleElement=E(),A()(D.Z,$),D.Z&&D.Z.locals&&D.Z.locals;var R=n(1900),F=n(5727),I=n.n(F),L=(0,R.Z)(b,void 0,void 0,!1,null,"3e2a42f5",null);"function"==typeof I()&&I()(L);const z=L.exports},9104:(e,t,n)=>{"use strict";function r(e){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r(e)}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;tS});const s={name:"NcButton",props:{disabled:{type:Boolean,default:!1},type:{type:String,validator:function(e){return-1!==["primary","secondary","tertiary","tertiary-no-background","tertiary-on-primary","error","warning","success"].indexOf(e)},default:"secondary"},nativeType:{type:String,validator:function(e){return-1!==["submit","reset","button"].indexOf(e)},default:"button"},wide:{type:Boolean,default:!1},ariaLabel:{type:String,default:null},href:{type:String,default:null},to:{type:[String,Object],default:null},exact:{type:Boolean,default:!1},ariaHidden:{type:Boolean,default:null}},render:function(e){var t,n,r,o,s,l=this,u=null===(t=this.$slots.default)||void 0===t||null===(n=t[0])||void 0===n||null===(r=n.text)||void 0===r||null===(o=r.trim)||void 0===o?void 0:o.call(r),c=!!u,p=null===(s=this.$slots)||void 0===s?void 0:s.icon;u||this.ariaLabel||console.warn("You need to fill either the text or the ariaLabel props in the button component.",{text:u,ariaLabel:this.ariaLabel},this);var d=function(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=n.navigate,o=n.isActive,s=n.isExactActive;return e(l.to||!l.href?"button":"a",{class:["button-vue",(t={"button-vue--icon-only":p&&!c,"button-vue--text-only":c&&!p,"button-vue--icon-and-text":p&&c},i(t,"button-vue--vue-".concat(l.type),l.type),i(t,"button-vue--wide",l.wide),i(t,"active",o),i(t,"router-link-exact-active",s),t)],attrs:a({"aria-label":l.ariaLabel,disabled:l.disabled,type:l.href?null:l.nativeType,role:l.href?"button":null,href:!l.to&&l.href?l.href:null},l.$attrs),on:a(a({},l.$listeners),{},{click:function(e){var t,n;null===(t=l.$listeners)||void 0===t||null===(n=t.click)||void 0===n||n.call(t,e),null==r||r(e)}})},[e("span",{class:"button-vue__wrapper"},[p?e("span",{class:"button-vue__icon",attrs:{"aria-hidden":l.ariaHidden}},[l.$slots.icon]):null,c?e("span",{class:"button-vue__text"},[u]):null])])};return this.to?e("router-link",{props:{custom:!0,to:this.to,exact:this.exact},scopedSlots:{default:d}}):d()}};var l=n(3379),u=n.n(l),c=n(7795),p=n.n(c),d=n(569),f=n.n(d),m=n(3565),v=n.n(m),h=n(9216),g=n.n(h),b=n(4589),y=n.n(b),A=n(1898),w={};w.styleTagTransform=y(),w.setAttributes=v(),w.insert=f().bind(null,"head"),w.domAPI=p(),w.insertStyleElement=g(),u()(A.Z,w),A.Z&&A.Z.locals&&A.Z.locals;var C=n(1900),x=n(2102),k=n.n(x),_=(0,C.Z)(s,void 0,void 0,!1,null,"2e49be1e",null);"function"==typeof k()&&k()(_);const S=_.exports},3335:(e,t,n)=>{"use strict";n.d(t,{default:()=>C});const r={name:"NcEmptyContent",props:{title:{type:String,default:""},description:{type:String,default:""}},computed:{hasTitle:function(){return""!==this.title},hasDescription:function(){return""!==this.description}}};var o=n(3379),a=n.n(o),i=n(7795),s=n.n(i),l=n(569),u=n.n(l),c=n(3565),p=n.n(c),d=n(9216),f=n.n(d),m=n(4589),v=n.n(m),h=n(3300),g={};g.styleTagTransform=v(),g.setAttributes=p(),g.insert=u().bind(null,"head"),g.domAPI=s(),g.insertStyleElement=f(),a()(h.Z,g),h.Z&&h.Z.locals&&h.Z.locals;var b=n(1900),y=n(9258),A=n.n(y),w=(0,b.Z)(r,(function(){var e=this,t=e._self._c;return t("div",{staticClass:"empty-content",attrs:{role:"note"}},[e.$slots.icon?t("div",{staticClass:"empty-content__icon",attrs:{"aria-hidden":"true"}},[e._t("icon")],2):e._e(),e._v(" "),e._t("title",(function(){return[e.hasTitle?t("h2",{staticClass:"empty-content__title"},[e._v("\n\t\t\t"+e._s(e.title)+"\n\t\t")]):e._e()]})),e._v(" "),e.hasDescription?t("p",[e._v("\n\t\t"+e._s(e.description)+"\n\t")]):e._e(),e._v(" "),e.$slots.action?t("div",{staticClass:"empty-content__action"},[e._t("action")],2):e._e()],2)}),[],!1,null,"04d732c3",null);"function"==typeof A()&&A()(w);const C=w.exports},5378:(e,t,n)=>{"use strict";n.d(t,{default:()=>C});const r={name:"NcLoadingIcon",props:{size:{type:Number,default:20},appearance:{type:String,validator:function(e){return["auto","light","dark"].includes(e)},default:"auto"},title:{type:String,default:""}},computed:{colors:function(){var e=["#777","#CCC"];return"light"===this.appearance?e:"dark"===this.appearance?e.reverse():["var(--color-loading-light)","var(--color-loading-dark)"]}}};var o=n(3379),a=n.n(o),i=n(7795),s=n.n(i),l=n(569),u=n.n(l),c=n(3565),p=n.n(c),d=n(9216),f=n.n(d),m=n(4589),v=n.n(m),h=n(5030),g={};g.styleTagTransform=v(),g.setAttributes=p(),g.insert=u().bind(null,"head"),g.domAPI=s(),g.insertStyleElement=f(),a()(h.Z,g),h.Z&&h.Z.locals&&h.Z.locals;var b=n(1900),y=n(9280),A=n.n(y),w=(0,b.Z)(r,(function(){var e=this,t=e._self._c;return t("span",{staticClass:"material-design-icon loading-icon",attrs:{"aria-label":e.title,role:"img"}},[t("svg",{attrs:{width:e.size,height:e.size,viewBox:"0 0 24 24"}},[t("path",{attrs:{fill:e.colors[0],d:"M12,4V2A10,10 0 1,0 22,12H20A8,8 0 1,1 12,4Z"}}),e._v(" "),t("path",{attrs:{fill:e.colors[1],d:"M12,4V2A10,10 0 0,1 22,12H20A8,8 0 0,0 12,4Z"}},[e.title?t("title",[e._v(e._s(e.title))]):e._e()])])])}),[],!1,null,"c4a9cada",null);"function"==typeof A()&&A()(w);const C=w.exports},5825:(e,t,n)=>{"use strict";n.d(t,{default:()=>T});var r=n(9454),o=n(4505),a=n(1206);function i(e){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},i(e)}function s(){s=function(){return e};var e={},t=Object.prototype,n=t.hasOwnProperty,r=Object.defineProperty||function(e,t,n){e[t]=n.value},o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",l=o.asyncIterator||"@@asyncIterator",u=o.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function p(e,t,n,o){var a=t&&t.prototype instanceof m?t:m,i=Object.create(a.prototype),s=new O(o||[]);return r(i,"_invoke",{value:x(e,n,s)}),i}function d(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}e.wrap=p;var f={};function m(){}function v(){}function h(){}var g={};c(g,a,(function(){return this}));var b=Object.getPrototypeOf,y=b&&b(b(E([])));y&&y!==t&&n.call(y,a)&&(g=y);var A=h.prototype=m.prototype=Object.create(g);function w(e){["next","throw","return"].forEach((function(t){c(e,t,(function(e){return this._invoke(t,e)}))}))}function C(e,t){function o(r,a,s,l){var u=d(e[r],e,a);if("throw"!==u.type){var c=u.arg,p=c.value;return p&&"object"==i(p)&&n.call(p,"__await")?t.resolve(p.__await).then((function(e){o("next",e,s,l)}),(function(e){o("throw",e,s,l)})):t.resolve(p).then((function(e){c.value=e,s(c)}),(function(e){return o("throw",e,s,l)}))}l(u.arg)}var a;r(this,"_invoke",{value:function(e,n){function r(){return new t((function(t,r){o(e,n,t,r)}))}return a=a?a.then(r,r):r()}})}function x(e,t,n){var r="suspendedStart";return function(o,a){if("executing"===r)throw new Error("Generator is already running");if("completed"===r){if("throw"===o)throw a;return{value:void 0,done:!0}}for(n.method=o,n.arg=a;;){var i=n.delegate;if(i){var s=k(i,n);if(s){if(s===f)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if("suspendedStart"===r)throw r="completed",n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r="executing";var l=d(e,t,n);if("normal"===l.type){if(r=n.done?"completed":"suspendedYield",l.arg===f)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(r="completed",n.method="throw",n.arg=l.arg)}}}function k(e,t){var n=t.method,r=e.iterator[n];if(void 0===r)return t.delegate=null,"throw"===n&&e.iterator.return&&(t.method="return",t.arg=void 0,k(e,t),"throw"===t.method)||"return"!==n&&(t.method="throw",t.arg=new TypeError("The iterator does not provide a '"+n+"' method")),f;var o=d(r,e.iterator,t.arg);if("throw"===o.type)return t.method="throw",t.arg=o.arg,t.delegate=null,f;var a=o.arg;return a?a.done?(t[e.resultName]=a.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,f):a:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,f)}function _(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function S(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function O(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(_,this),this.reset(!0)}function E(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,o=function t(){for(;++r=0;--o){var a=this.tryEntries[o],i=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var s=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(s&&l){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),S(n),f}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;S(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:E(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=void 0),f}},e}function l(e,t,n,r,o,a,i){try{var s=e[a](i),l=s.value}catch(e){return void n(e)}s.done?t(l):Promise.resolve(l).then(r,o)}const u={name:"NcPopover",components:{Dropdown:r.Dropdown},inheritAttrs:!1,props:{popoverBaseClass:{type:String,default:""},focusTrap:{type:Boolean,default:!0},setReturnFocus:{default:void 0,type:[HTMLElement,SVGElement,String,Boolean]}},emits:["after-show","after-hide"],beforeDestroy:function(){this.clearFocusTrap()},methods:{useFocusTrap:function(){var e,t=this;return(e=s().mark((function e(){var n,r,i;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t.$nextTick();case 2:if(t.focusTrap){e.next=4;break}return e.abrupt("return");case 4:if(i=null===(n=t.$refs.popover)||void 0===n||null===(r=n.$refs.popperContent)||void 0===r?void 0:r.$el){e.next=7;break}return e.abrupt("return");case 7:t.$focusTrap=(0,o.createFocusTrap)(i,{escapeDeactivates:!1,allowOutsideClick:!0,setReturnFocus:t.setReturnFocus,trapStack:(0,a.L)()}),t.$focusTrap.activate();case 9:case"end":return e.stop()}}),e)})),function(){var t=this,n=arguments;return new Promise((function(r,o){var a=e.apply(t,n);function i(e){l(a,r,o,i,s,"next",e)}function s(e){l(a,r,o,i,s,"throw",e)}i(void 0)}))})()},clearFocusTrap:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{var t;null===(t=this.$focusTrap)||void 0===t||t.deactivate(e),this.$focusTrap=null}catch(e){console.warn(e)}},afterShow:function(){var e=this;this.$nextTick((function(){e.$emit("after-show"),e.useFocusTrap()}))},afterHide:function(){this.$emit("after-hide"),this.clearFocusTrap()}}},c=u;var p=n(3379),d=n.n(p),f=n(7795),m=n.n(f),v=n(569),h=n.n(v),g=n(3565),b=n.n(g),y=n(9216),A=n.n(y),w=n(4589),C=n.n(w),x=n(4401),k={};k.styleTagTransform=C(),k.setAttributes=b(),k.insert=h().bind(null,"head"),k.domAPI=m(),k.insertStyleElement=A(),d()(x.Z,k),x.Z&&x.Z.locals&&x.Z.locals;var _=n(1900),S=n(2405),O=n.n(S),E=(0,_.Z)(c,(function(){var e=this;return(0,e._self._c)("Dropdown",e._g(e._b({ref:"popover",attrs:{distance:10,"arrow-padding":10,"no-auto-focus":!0,"popper-class":e.popoverBaseClass},on:{"apply-show":e.afterShow,"apply-hide":e.afterHide},scopedSlots:e._u([{key:"popper",fn:function(){return[e._t("default")]},proxy:!0}],null,!0)},"Dropdown",e.$attrs,!1),e.$listeners),[e._t("trigger")],2)}),[],!1,null,null,null);"function"==typeof O()&&O()(E);const T=E.exports},3329:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});const r={name:"NcVNodes",props:{vnodes:{type:[Array,Object],default:null}},render:function(e){var t,n,r;return this.vnodes||(null===(t=this.$slots)||void 0===t?void 0:t.default)||(null===(n=this.$scopedSlots)||void 0===n||null===(r=n.default)||void 0===r?void 0:r.call(n))}},o=(0,n(1900).Z)(r,void 0,void 0,!1,null,null,null).exports},8167:(e,t,n)=>{"use strict";n.d(t,{default:()=>r});const r={inserted:function(e){e.focus()}}},5675:(e,t,n)=>{"use strict";n.d(t,{default:()=>o});var r=n(1390);const o=function(e,t){var n;!0===(null===(n=t.value)||void 0===n?void 0:n.linkify)&&(e.innerHTML=(0,r.Z)(t.value.text))}},336:(e,t,n)=>{"use strict";n.d(t,{default:()=>b});var r=n(9454),o=n(3379),a=n.n(o),i=n(7795),s=n.n(i),l=n(569),u=n.n(l),c=n(3565),p=n.n(c),d=n(9216),f=n.n(d),m=n(4589),v=n.n(m),h=n(8384),g={};g.styleTagTransform=v(),g.setAttributes=p(),g.insert=u().bind(null,"head"),g.domAPI=s(),g.insertStyleElement=f(),a()(h.Z,g),h.Z&&h.Z.locals&&h.Z.locals,r.options.themes.tooltip.html=!1,r.options.themes.tooltip.delay={show:500,hide:200},r.options.themes.tooltip.distance=10,r.options.themes.tooltip["arrow-padding"]=3;const b=r.VTooltip},932:(e,t,n)=>{"use strict";n.d(t,{t:()=>a});var r=(0,n(7931).getGettextBuilder)().detectLocale();[{locale:"ar",translations:{"{tag} (invisible)":"{tag} (غير مرئي)","{tag} (restricted)":"{tag} (مقيد)",Actions:"الإجراءات",Activities:"النشاطات","Animals & Nature":"الحيوانات والطبيعة","Anything shared with the same group of people will show up here":"أي مادة تمت مشاركتها مع نفس المجموعة من الأشخاص سيتم عرضها هنا","Avatar of {displayName}":"صورة {displayName} الرمزية","Avatar of {displayName}, {status}":"صورة {displayName} الرمزية، {status}","Cancel changes":"إلغاء التغييرات","Change title":"تغيير العنوان",Choose:"إختيار","Clear text":"مسح النص",Close:"أغلق","Close modal":"قفل الشرط","Close navigation":"إغلاق المتصفح","Close sidebar":"قفل الشريط الجانبي","Confirm changes":"تأكيد التغييرات",Custom:"مخصص","Edit item":"تعديل عنصر","Error getting related resources":"خطأ في تحصيل مصادر ذات صلة","External documentation for {title}":"الوثائق الخارجية لـ{title}",Favorite:"مفضلة",Flags:"الأعلام","Food & Drink":"الطعام والشراب","Frequently used":"كثيرا ما تستخدم",Global:"عالمي","Go back to the list":"العودة إلى القائمة","Hide password":"إخفاء كلمة السر","Message limit of {count} characters reached":"تم الوصول إلى الحد الأقصى لعدد الأحرف في الرسالة: {count} حرف","More items …":"عناصر أخرى ...",Next:"التالي","No emoji found":"لم يتم العثور على أي رمز تعبيري","No results":"ليس هناك أية نتيجة",Objects:"الأشياء",Open:"فتح",'Open link to "{resourceTitle}"':'فتح رابط إلى "{resourceTitle}"',"Open navigation":"فتح المتصفح","Password is secure":"كلمة السر مُؤمّنة","Pause slideshow":"إيقاف العرض مؤقتًا","People & Body":"الناس والجسم","Pick an emoji":"اختر رمزًا تعبيريًا","Please select a time zone:":"الرجاء تحديد المنطقة الزمنية:",Previous:"السابق","Related resources":"مصادر ذات صلة",Search:"بحث","Search results":"نتائج البحث","Select a tag":"اختر علامة",Settings:"الإعدادات","Settings navigation":"إعدادات المتصفح","Show password":"أعرض كلمة السر","Smileys & Emotion":"الوجوه و الرموز التعبيرية","Start slideshow":"بدء العرض",Submit:"إرسال",Symbols:"الرموز","Travel & Places":"السفر والأماكن","Type to search time zone":"اكتب للبحث عن منطقة زمنية","Unable to search the group":"تعذر البحث في المجموعة","Undo changes":"التراجع عن التغييرات","Write message, @ to mention someone, : for emoji autocompletion …":"اكتب رسالة، @ للإشارة إلى شخص ما، : للإكمال التلقائي للرموز التعبيرية ..."}},{locale:"br",translations:{"{tag} (invisible)":"{tag} (diwelus)","{tag} (restricted)":"{tag} (bevennet)",Actions:"Oberioù",Activities:"Oberiantizoù","Animals & Nature":"Loened & Natur",Choose:"Dibab",Close:"Serriñ",Custom:"Personelañ",Flags:"Bannieloù","Food & Drink":"Boued & Evajoù","Frequently used":"Implijet alies",Next:"Da heul","No emoji found":"Emoji ebet kavet","No results":"Disoc'h ebet",Objects:"Traoù","Pause slideshow":"Arsav an diaporama","People & Body":"Tud & Korf","Pick an emoji":"Choaz un emoji",Previous:"A-raok",Search:"Klask","Search results":"Disoc'hoù an enklask","Select a tag":"Choaz ur c'hlav",Settings:"Arventennoù","Smileys & Emotion":"Smileyioù & Fromoù","Start slideshow":"Kregiñ an diaporama",Symbols:"Arouezioù","Travel & Places":"Beaj & Lec'hioù","Unable to search the group":"Dibosupl eo klask ar strollad"}},{locale:"ca",translations:{"{tag} (invisible)":"{tag} (invisible)","{tag} (restricted)":"{tag} (restringit)",Actions:"Accions",Activities:"Activitats","Animals & Nature":"Animals i natura","Anything shared with the same group of people will show up here":"Qualsevol cosa compartida amb el mateix grup de persones es mostrarà aquí","Avatar of {displayName}":"Avatar de {displayName}","Avatar of {displayName}, {status}":"Avatar de {displayName}, {status}","Cancel changes":"Cancel·la els canvis","Change title":"Canviar títol",Choose:"Tria","Clear text":"Netejar text",Close:"Tanca","Close modal":"Tancar el mode","Close navigation":"Tanca la navegació","Close sidebar":"Tancar la barra lateral","Confirm changes":"Confirmeu els canvis",Custom:"Personalitzat","Edit item":"Edita l'element","Error getting related resources":"Error obtenint els recursos relacionats","Error parsing svg":"Error en l'anàlisi del svg","External documentation for {title}":"Documentació externa per a {title}",Favorite:"Preferit",Flags:"Marques","Food & Drink":"Menjar i begudes","Frequently used":"Utilitzats recentment",Global:"Global","Go back to the list":"Torna a la llista","Hide password":"Amagar contrasenya","Message limit of {count} characters reached":"S'ha arribat al límit de {count} caràcters per missatge","More items …":"Més artícles...",Next:"Següent","No emoji found":"No s'ha trobat cap emoji","No results":"Sense resultats",Objects:"Objectes",Open:"Obrir",'Open link to "{resourceTitle}"':'Obrir enllaç a "{resourceTitle}"',"Open navigation":"Obre la navegació","Password is secure":"Contrasenya segura
","Pause slideshow":"Atura la presentació","People & Body":"Persones i cos","Pick an emoji":"Trieu un emoji","Please select a time zone:":"Seleccioneu una zona horària:",Previous:"Anterior","Related resources":"Recursos relacionats",Search:"Cerca","Search results":"Resultats de cerca","Select a tag":"Seleccioneu una etiqueta",Settings:"Paràmetres","Settings navigation":"Navegació d'opcions","Show password":"Mostrar contrasenya","Smileys & Emotion":"Cares i emocions","Start slideshow":"Inicia la presentació",Submit:"Envia",Symbols:"Símbols","Travel & Places":"Viatges i llocs","Type to search time zone":"Escriviu per cercar la zona horària","Unable to search the group":"No es pot cercar el grup","Undo changes":"Desfés els canvis",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Escriu missatge, fes servir "@" per esmentar algú, fes servir ":" per autocompletar emojis...'}},{locale:"cs_CZ",translations:{"{tag} (invisible)":"{tag} (neviditelné)","{tag} (restricted)":"{tag} (omezené)",Actions:"Akce",Activities:"Aktivity","Animals & Nature":"Zvířata a příroda","Anything shared with the same group of people will show up here":"Cokoli nasdíleného stejné skupině lidí se zobrazí zde","Avatar of {displayName}":"Zástupný obrázek uživatele {displayName}","Avatar of {displayName}, {status}":"Zástupný obrázek uživatele {displayName}, {status}","Cancel changes":"Zrušit změny","Change title":"Změnit nadpis",Choose:"Zvolit","Clear text":"Čitelný text",Close:"Zavřít","Close modal":"Zavřít dialogové okno","Close navigation":"Zavřít navigaci","Close sidebar":"Zavřít postranní panel","Confirm changes":"Potvrdit změny",Custom:"Uživatelsky určené","Edit item":"Upravit položku","Error getting related resources":"Chyba při získávání souvisejících prostředků","Error parsing svg":"Chyba při zpracovávání svg","External documentation for {title}":"Externí dokumentace k {title}",Favorite:"Oblíbené",Flags:"Příznaky","Food & Drink":"Jídlo a pití","Frequently used":"Často používané",Global:"Globální","Go back to the list":"Jít zpět na seznam","Hide password":"Skrýt heslo","Message limit of {count} characters reached":"Dosaženo limitu počtu ({count}) znaků zprávy","More items …":"Další položky…",Next:"Následující","No emoji found":"Nenalezeno žádné emoji","No results":"Nic nenalezeno",Objects:"Objekty",Open:"Otevřít",'Open link to "{resourceTitle}"':"Otevřít odkaz na „{resourceTitle}“","Open navigation":"Otevřít navigaci","Password is secure":"Heslo je bezpečné","Pause slideshow":"Pozastavit prezentaci","People & Body":"Lidé a tělo","Pick an emoji":"Vybrat emoji","Please select a time zone:":"Vyberte časovou zónu:",Previous:"Předchozí","Related resources":"Související prostředky",Search:"Hledat","Search results":"Výsledky hledání","Select a tag":"Vybrat štítek",Settings:"Nastavení","Settings navigation":"Pohyb po nastavení","Show password":"Zobrazit heslo","Smileys & Emotion":"Úsměvy a emoce","Start slideshow":"Spustit prezentaci",Submit:"Odeslat",Symbols:"Symboly","Travel & Places":"Cestování a místa","Type to search time zone":"Psaním vyhledejte časovou zónu","Unable to search the group":"Nedaří se hledat skupinu","Undo changes":"Vzít změny zpět",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':"Napište zprávu – pokud chcete někoho zmínit, napište před jeho uživatelským jménem „@“ (zavináč); automatické doplňování emotikonů zahájíte napsáním „:“ (dvojtečky)…"}},{locale:"da",translations:{"{tag} (invisible)":"{tag} (usynlig)","{tag} (restricted)":"{tag} (begrænset)",Actions:"Handlinger",Activities:"Aktiviteter","Animals & Nature":"Dyr & Natur","Anything shared with the same group of people will show up here":"Alt der deles med samme gruppe af personer vil vises her","Avatar of {displayName}":"Avatar af {displayName}","Avatar of {displayName}, {status}":"Avatar af {displayName}, {status}","Cancel changes":"Annuller ændringer","Change title":"Ret titel",Choose:"Vælg","Clear text":"Ryd tekst",Close:"Luk","Close modal":"Luk vindue","Close navigation":"Luk navigation","Close sidebar":"Luk sidepanel","Confirm changes":"Bekræft ændringer",Custom:"Brugerdefineret","Edit item":"Rediger emne","Error getting related resources":"Kunne ikke hente tilknyttede data","Error parsing svg":"Fejl ved analysering af svg","External documentation for {title}":"Ekstern dokumentation for {title}",Favorite:"Favorit",Flags:"Flag","Food & Drink":"Mad & Drikke","Frequently used":"Ofte brugt",Global:"Global","Go back to the list":"Tilbage til listen","Hide password":"Skjul kodeord","Message limit of {count} characters reached":"Begrænsning på {count} tegn er nået","More items …":"Mere ...",Next:"Videre","No emoji found":"Ingen emoji fundet","No results":"Ingen resultater",Objects:"Objekter",Open:"Åbn",'Open link to "{resourceTitle}"':'Åbn link til "{resourceTitle}"',"Open navigation":"Åbn navigation","Password is secure":"Kodeordet er sikkert","Pause slideshow":"Suspender fremvisning","People & Body":"Mennesker & Menneskekroppen","Pick an emoji":"Vælg en emoji","Please select a time zone:":"Vælg venligst en tidszone:",Previous:"Forrige","Related resources":"Relaterede emner",Search:"Søg","Search results":"Søgeresultater","Select a tag":"Vælg et mærke",Settings:"Indstillinger","Settings navigation":"Naviger i indstillinger","Show password":"Vis kodeord","Smileys & Emotion":"Smileys & Emotion","Start slideshow":"Start fremvisning",Submit:"Send",Symbols:"Symboler","Travel & Places":"Rejser & Rejsemål","Type to search time zone":"Indtast for at søge efter tidszone","Unable to search the group":"Kan ikke søge på denne gruppe","Undo changes":"Fortryd ændringer",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Skriv besked, brug "@" for at nævne nogen, brug ":" til emoji-autofuldførelse ...'}},{locale:"de",translations:{"{tag} (invisible)":"{tag} (unsichtbar)","{tag} (restricted)":"{tag} (eingeschränkt)",Actions:"Aktionen",Activities:"Aktivitäten","Animals & Nature":"Tiere & Natur","Anything shared with the same group of people will show up here":"Alles, das mit derselben Gruppe von Personen geteilt wird, wird hier angezeigt","Avatar of {displayName}":"Avatar von {displayName}","Avatar of {displayName}, {status}":"Avatar von {displayName}, {status}","Cancel changes":"Änderungen verwerfen","Change title":"Titel ändern",Choose:"Auswählen","Clear text":"Klartext",Close:"Schließen","Close modal":"Modal schließen","Close navigation":"Navigation schließen","Close sidebar":"Seitenleiste schließen","Confirm changes":"Änderungen bestätigen",Custom:"Benutzerdefiniert","Edit item":"Objekt bearbeiten","Error getting related resources":"Fehler beim Abrufen verwandter Ressourcen","Error parsing svg":"Fehler beim Einlesen der SVG","External documentation for {title}":"Externe Dokumentation für {title}",Favorite:"Favorit",Flags:"Flaggen","Food & Drink":"Essen & Trinken","Frequently used":"Häufig verwendet",Global:"Global","Go back to the list":"Zurück zur Liste","Hide password":"Passwort verbergen","Message limit of {count} characters reached":"Nachrichtenlimit von {count} Zeichen erreicht","More items …":"Weitere Elemente …",Next:"Weiter","No emoji found":"Kein Emoji gefunden","No results":"Keine Ergebnisse",Objects:"Gegenstände",Open:"Öffnen",'Open link to "{resourceTitle}"':'Link zu "{resourceTitle}" öffnen',"Open navigation":"Navigation öffnen","Password is secure":"Passwort ist sicher","Pause slideshow":"Diashow pausieren","People & Body":"Menschen & Körper","Pick an emoji":"Ein Emoji auswählen","Please select a time zone:":"Bitte wählen Sie eine Zeitzone:",Previous:"Vorherige","Related resources":"Verwandte Ressourcen",Search:"Suche","Search results":"Suchergebnisse","Select a tag":"Schlagwort auswählen",Settings:"Einstellungen","Settings navigation":"Einstellungen für die Navigation","Show password":"Passwort anzeigen","Smileys & Emotion":"Smileys & Emotionen","Start slideshow":"Diashow starten",Submit:"Einreichen",Symbols:"Symbole","Travel & Places":"Reisen & Orte","Type to search time zone":"Tippen, um Zeitzone zu suchen","Unable to search the group":"Die Gruppe konnte nicht durchsucht werden","Undo changes":"Änderungen rückgängig machen",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Nachricht schreiben, "@" um jemanden zu erwähnen, ":" für die automatische Vervollständigung von Emojis …'}},{locale:"de_DE",translations:{"{tag} (invisible)":"{tag} (unsichtbar)","{tag} (restricted)":"{tag} (eingeschränkt)",Actions:"Aktionen",Activities:"Aktivitäten","Animals & Nature":"Tiere & Natur","Anything shared with the same group of people will show up here":"Alles, das mit derselben Gruppe von Personen geteilt wird, wird hier angezeigt","Avatar of {displayName}":"Avatar von {displayName}","Avatar of {displayName}, {status}":"Avatar von {displayName}, {status}","Cancel changes":"Änderungen verwerfen","Change title":"Titel ändern",Choose:"Auswählen","Clear text":"Klartext",Close:"Schließen","Close modal":"Modal schließen","Close navigation":"Navigation schließen","Close sidebar":"Seitenleiste schließen","Confirm changes":"Änderungen bestätigen",Custom:"Benutzerdefiniert","Edit item":"Objekt bearbeiten","Error getting related resources":"Fehler beim Abrufen verwandter Ressourcen","Error parsing svg":"Fehler beim Einlesen der SVG","External documentation for {title}":"Externe Dokumentation für {title}",Favorite:"Favorit",Flags:"Flaggen","Food & Drink":"Essen & Trinken","Frequently used":"Häufig verwendet",Global:"Global","Go back to the list":"Zurück zur Liste","Hide password":"Passwort verbergen","Message limit of {count} characters reached":"Nachrichtenlimit von {count} Zeichen erreicht","More items …":"Weitere Elemente …",Next:"Weiter","No emoji found":"Kein Emoji gefunden","No results":"Keine Ergebnisse",Objects:"Objekte",Open:"Öffnen",'Open link to "{resourceTitle}"':'Link zu "{resourceTitle}" öffnen',"Open navigation":"Navigation öffnen","Password is secure":"Passwort ist sicher","Pause slideshow":"Diashow pausieren","People & Body":"Menschen & Körper","Pick an emoji":"Ein Emoji auswählen","Please select a time zone:":"Bitte eine Zeitzone auswählen:",Previous:"Vorherige","Related resources":"Verwandte Ressourcen",Search:"Suche","Search results":"Suchergebnisse","Select a tag":"Schlagwort auswählen",Settings:"Einstellungen","Settings navigation":"Einstellungen für die Navigation","Show password":"Passwort anzeigen","Smileys & Emotion":"Smileys & Emotionen","Start slideshow":"Diashow starten",Submit:"Einreichen",Symbols:"Symbole","Travel & Places":"Reisen & Orte","Type to search time zone":"Tippen, um eine Zeitzone zu suchen","Unable to search the group":"Die Gruppe kann nicht durchsucht werden","Undo changes":"Änderungen rückgängig machen",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Nachricht schreiben, "@" um jemanden zu erwähnen, ":" für die automatische Vervollständigung von Emojis …'}},{locale:"el",translations:{"{tag} (invisible)":"{tag} (αόρατο)","{tag} (restricted)":"{tag} (περιορισμένο)",Actions:"Ενέργειες",Activities:"Δραστηριότητες","Animals & Nature":"Ζώα & Φύση","Anything shared with the same group of people will show up here":"Οτιδήποτε μοιράζεται με την ίδια ομάδα ατόμων θα εμφανίζεται εδώ","Avatar of {displayName}":"Άβαταρ του {displayName}","Avatar of {displayName}, {status}":"Άβαταρ του {displayName}, {status}","Cancel changes":"Ακύρωση αλλαγών","Change title":"Αλλαγή τίτλου",Choose:"Επιλογή","Clear text":"Εκκαθάριση κειμένου",Close:"Κλείσιμο","Close modal":"Βοηθητικό κλείσιμο","Close navigation":"Κλείσιμο πλοήγησης","Close sidebar":"Κλείσιμο πλευρικής μπάρας","Confirm changes":"Επιβεβαίωση αλλαγών",Custom:"Προσαρμογή","Edit item":"Επεξεργασία","Error getting related resources":"Σφάλμα λήψης σχετικών πόρων","Error parsing svg":"Σφάλμα ανάλυσης svg","External documentation for {title}":"Εξωτερική τεκμηρίωση για {title}",Favorite:"Αγαπημένα",Flags:"Σημαίες","Food & Drink":"Φαγητό & Ποτό","Frequently used":"Συχνά χρησιμοποιούμενο",Global:"Καθολικό","Go back to the list":"Επιστροφή στην αρχική λίστα ","Hide password":"Απόκρυψη κωδικού πρόσβασης","Message limit of {count} characters reached":"Συμπληρώθηκε το όριο των {count} χαρακτήρων του μηνύματος","More items …":"Περισσότερα στοιχεία …",Next:"Επόμενο","No emoji found":"Δεν βρέθηκε emoji","No results":"Κανένα αποτέλεσμα",Objects:"Αντικείμενα",Open:"Άνοιγμα",'Open link to "{resourceTitle}"':'Άνοιγμα συνδέσμου στο "{resourceTitle}"',"Open navigation":"Άνοιγμα πλοήγησης","Password is secure":"Ο κωδικός πρόσβασης είναι ασφαλής","Pause slideshow":"Παύση προβολής διαφανειών","People & Body":"Άνθρωποι & Σώμα","Pick an emoji":"Επιλέξτε ένα emoji","Please select a time zone:":"Παρακαλούμε επιλέξτε μια ζώνη ώρας:",Previous:"Προηγούμενο","Related resources":"Σχετικοί πόροι",Search:"Αναζήτηση","Search results":"Αποτελέσματα αναζήτησης","Select a tag":"Επιλογή ετικέτας",Settings:"Ρυθμίσεις","Settings navigation":"Πλοήγηση ρυθμίσεων","Show password":"Εμφάνιση κωδικού πρόσβασης","Smileys & Emotion":"Φατσούλες & Συναίσθημα","Start slideshow":"Έναρξη προβολής διαφανειών",Submit:"Υποβολή",Symbols:"Σύμβολα","Travel & Places":"Ταξίδια & Τοποθεσίες","Type to search time zone":"Πληκτρολογήστε για αναζήτηση ζώνης ώρας","Unable to search the group":"Δεν είναι δυνατή η αναζήτηση της ομάδας","Undo changes":"Αναίρεση Αλλαγών",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Γράψτε μήνυμα, χρησιμοποιείστε "@" για να αναφέρετε κάποιον, χρησιμοποιείστε ":" για αυτόματη συμπλήρωση emoji …'}},{locale:"en_GB",translations:{"{tag} (invisible)":"{tag} (invisible)","{tag} (restricted)":"{tag} (restricted)",Actions:"Actions",Activities:"Activities","Animals & Nature":"Animals & Nature","Anything shared with the same group of people will show up here":"Anything shared with the same group of people will show up here","Avatar of {displayName}":"Avatar of {displayName}","Avatar of {displayName}, {status}":"Avatar of {displayName}, {status}","Cancel changes":"Cancel changes","Change title":"Change title",Choose:"Choose","Clear text":"Clear text",Close:"Close","Close modal":"Close modal","Close navigation":"Close navigation","Close sidebar":"Close sidebar","Confirm changes":"Confirm changes",Custom:"Custom","Edit item":"Edit item","Error getting related resources":"Error getting related resources","Error parsing svg":"Error parsing svg","External documentation for {title}":"External documentation for {title}",Favorite:"Favourite",Flags:"Flags","Food & Drink":"Food & Drink","Frequently used":"Frequently used",Global:"Global","Go back to the list":"Go back to the list","Hide password":"Hide password","Message limit of {count} characters reached":"Message limit of {count} characters reached","More items …":"More items …",Next:"Next","No emoji found":"No emoji found","No results":"No results",Objects:"Objects",Open:"Open",'Open link to "{resourceTitle}"':'Open link to "{resourceTitle}"',"Open navigation":"Open navigation","Password is secure":"Password is secure","Pause slideshow":"Pause slideshow","People & Body":"People & Body","Pick an emoji":"Pick an emoji","Please select a time zone:":"Please select a time zone:",Previous:"Previous","Related resources":"Related resources",Search:"Search","Search results":"Search results","Select a tag":"Select a tag",Settings:"Settings","Settings navigation":"Settings navigation","Show password":"Show password","Smileys & Emotion":"Smileys & Emotion","Start slideshow":"Start slideshow",Submit:"Submit",Symbols:"Symbols","Travel & Places":"Travel & Places","Type to search time zone":"Type to search time zone","Unable to search the group":"Unable to search the group","Undo changes":"Undo changes",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Write message, use "@" to mention someone, use ":" for emoji autocompletion …'}},{locale:"eo",translations:{"{tag} (invisible)":"{tag} (kaŝita)","{tag} (restricted)":"{tag} (limigita)",Actions:"Agoj",Activities:"Aktiveco","Animals & Nature":"Bestoj & Naturo",Choose:"Elektu",Close:"Fermu",Custom:"Propra",Flags:"Flagoj","Food & Drink":"Manĝaĵo & Trinkaĵo","Frequently used":"Ofte uzataj","Message limit of {count} characters reached":"La limo je {count} da literoj atingita",Next:"Sekva","No emoji found":"La emoĝio forestas","No results":"La rezulto forestas",Objects:"Objektoj","Pause slideshow":"Payzi bildprezenton","People & Body":"Homoj & Korpo","Pick an emoji":"Elekti emoĝion ",Previous:"Antaŭa",Search:"Serĉi","Search results":"Serĉrezultoj","Select a tag":"Elektu etikedon",Settings:"Agordo","Settings navigation":"Agorda navigado","Smileys & Emotion":"Ridoj kaj Emocioj","Start slideshow":"Komenci bildprezenton",Symbols:"Signoj","Travel & Places":"Vojaĵoj & Lokoj","Unable to search the group":"Ne eblas serĉi en la grupo","Write message, @ to mention someone …":"Mesaĝi, uzu @ por mencii iun ..."}},{locale:"es",translations:{"{tag} (invisible)":"{tag} (invisible)","{tag} (restricted)":"{tag} (restringido)",Actions:"Acciones",Activities:"Actividades","Animals & Nature":"Animales y naturaleza","Anything shared with the same group of people will show up here":"Cualquier cosa que sea compartida con el mismo grupo de personas se mostrará aquí","Avatar of {displayName}":"Avatar de {displayName}","Avatar of {displayName}, {status}":"Avatar de {displayName}, {status}","Cancel changes":"Cancelar cambios","Change title":"Cambiar título",Choose:"Elegir","Clear text":"Limpiar texto",Close:"Cerrar","Close modal":"Cerrar modal","Close navigation":"Cerrar navegación","Close sidebar":"Cerrar barra lateral","Confirm changes":"Confirmar cambios",Custom:"Personalizado","Edit item":"Editar elemento","Error getting related resources":"Se encontró un error al obtener los recursos relacionados","Error parsing svg":"Error procesando svg","External documentation for {title}":"Documentacion externa de {title}",Favorite:"Favorito",Flags:"Banderas","Food & Drink":"Comida y bebida","Frequently used":"Usado con frecuenca",Global:"Global","Go back to the list":"Volver a la lista","Hide password":"Ocultar contraseña","Message limit of {count} characters reached":"El mensaje ha alcanzado el límite de {count} caracteres","More items …":"Más ítems...",Next:"Siguiente","No emoji found":"No hay ningún emoji","No results":" Ningún resultado",Objects:"Objetos",Open:"Abrir",'Open link to "{resourceTitle}"':'Abrir enlace a "{resourceTitle}"',"Open navigation":"Abrir navegación","Password is secure":"La contraseña es segura","Pause slideshow":"Pausar la presentación ","People & Body":"Personas y cuerpos","Pick an emoji":"Elegir un emoji","Please select a time zone:":"Por favor elige un huso de horario:",Previous:"Anterior","Related resources":"Recursos relacionados",Search:"Buscar","Search results":"Resultados de la búsqueda","Select a tag":"Seleccione una etiqueta",Settings:"Ajustes","Settings navigation":"Navegación por ajustes","Show password":"Mostrar contraseña","Smileys & Emotion":"Smileys y emoticonos","Start slideshow":"Iniciar la presentación",Submit:"Enviar",Symbols:"Símbolos","Travel & Places":"Viajes y lugares","Type to search time zone":"Escribe para buscar un huso de horario","Unable to search the group":"No es posible buscar en el grupo","Undo changes":"Deshacer cambios",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Escribir mensaje, utilice "@" para mencionar a alguien, utilice ":" para autocompletado de emojis ...'}},{locale:"eu",translations:{"{tag} (invisible)":"{tag} (ikusezina)","{tag} (restricted)":"{tag} (mugatua)",Actions:"Ekintzak",Activities:"Jarduerak","Animals & Nature":"Animaliak eta Natura","Anything shared with the same group of people will show up here":"Pertsona-talde berarekin partekatutako edozer agertuko da hemen","Avatar of {displayName}":"{displayName}-(e)n irudia","Avatar of {displayName}, {status}":"{displayName} -(e)n irudia, {status}","Cancel changes":"Ezeztatu aldaketak","Change title":"Aldatu titulua",Choose:"Aukeratu","Clear text":"Garbitu testua",Close:"Itxi","Close modal":"Itxi modala","Close navigation":"Itxi nabigazioa","Close sidebar":"Itxi albo-barra","Confirm changes":"Baieztatu aldaketak",Custom:"Pertsonalizatua","Edit item":"Editatu elementua","Error getting related resources":"Errorea erlazionatutako baliabideak lortzerakoan","Error parsing svg":"Errore bat gertatu da svg-a analizatzean","External documentation for {title}":"Kanpoko dokumentazioa {title}(r)entzat",Favorite:"Gogokoa",Flags:"Banderak","Food & Drink":"Janaria eta edariak","Frequently used":"Askotan erabilia",Global:"Globala","Go back to the list":"Bueltatu zerrendara","Hide password":"Ezkutatu pasahitza","Message limit of {count} characters reached":"Mezuaren {count} karaketere-limitera heldu zara","More items …":"Elementu gehiago …",Next:"Hurrengoa","No emoji found":"Ez da emojirik aurkitu","No results":"Emaitzarik ez",Objects:"Objektuak",Open:"Ireki",'Open link to "{resourceTitle}"':'Ireki esteka: "{resourceTitle}"',"Open navigation":"Ireki nabigazioa","Password is secure":"Pasahitza segurua da","Pause slideshow":"Pausatu diaporama","People & Body":"Jendea eta gorputza","Pick an emoji":"Hautatu emoji bat","Please select a time zone:":"Mesedez hautatu ordu-zona bat:",Previous:"Aurrekoa","Related resources":"Erlazionatutako baliabideak",Search:"Bilatu","Search results":"Bilaketa emaitzak","Select a tag":"Hautatu etiketa bat",Settings:"Ezarpenak","Settings navigation":"Nabigazio ezarpenak","Show password":"Erakutsi pasahitza","Smileys & Emotion":"Smileyak eta emozioa","Start slideshow":"Hasi diaporama",Submit:"Bidali",Symbols:"Sinboloak","Travel & Places":"Bidaiak eta lekuak","Type to search time zone":"Idatzi ordu-zona bat bilatzeko","Unable to search the group":"Ezin izan da taldea bilatu","Undo changes":"Aldaketak desegin",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Idatzi mezua, erabili "@" norbait aipatzeko, erabili ":" emojiak automatikoki osatzeko...'}},{locale:"fi_FI",translations:{"{tag} (invisible)":"{tag} (näkymätön)","{tag} (restricted)":"{tag} (rajoitettu)",Actions:"Toiminnot",Activities:"Aktiviteetit","Animals & Nature":"Eläimet & luonto","Avatar of {displayName}":"Käyttäjän {displayName} avatar","Avatar of {displayName}, {status}":"Käyttäjän {displayName} avatar, {status}","Cancel changes":"Peruuta muutokset",Choose:"Valitse",Close:"Sulje","Close navigation":"Sulje navigaatio","Confirm changes":"Vahvista muutokset",Custom:"Mukautettu","Edit item":"Muokkaa kohdetta","External documentation for {title}":"Ulkoinen dokumentaatio kohteelle {title}",Flags:"Liput","Food & Drink":"Ruoka & juoma","Frequently used":"Usein käytetyt",Global:"Yleinen","Go back to the list":"Siirry takaisin listaan","Message limit of {count} characters reached":"Viestin merkken enimmäisimäärä {count} täynnä ",Next:"Seuraava","No emoji found":"Emojia ei löytynyt","No results":"Ei tuloksia",Objects:"Esineet & asiat","Open navigation":"Avaa navigaatio","Pause slideshow":"Keskeytä diaesitys","People & Body":"Ihmiset & keho","Pick an emoji":"Valitse emoji","Please select a time zone:":"Valitse aikavyöhyke:",Previous:"Edellinen",Search:"Etsi","Search results":"Hakutulokset","Select a tag":"Valitse tagi",Settings:"Asetukset","Settings navigation":"Asetusnavigaatio","Smileys & Emotion":"Hymiöt & tunteet","Start slideshow":"Aloita diaesitys",Submit:"Lähetä",Symbols:"Symbolit","Travel & Places":"Matkustus & kohteet","Type to search time zone":"Kirjoita etsiäksesi aikavyöhyke","Unable to search the group":"Ryhmää ei voi hakea","Undo changes":"Kumoa muutokset","Write message, @ to mention someone, : for emoji autocompletion …":"Kirjoita viesti, @ mainitaksesi käyttäjän, : emojin automaattitäydennykseen…"}},{locale:"fr",translations:{"{tag} (invisible)":"{tag} (invisible)","{tag} (restricted)":"{tag} (restreint)",Actions:"Actions",Activities:"Activités","Animals & Nature":"Animaux & Nature","Anything shared with the same group of people will show up here":"Tout ce qui est partagé avec le même groupe de personnes apparaîtra ici","Avatar of {displayName}":"Avatar de {displayName}","Avatar of {displayName}, {status}":"Avatar de {displayName}, {status}","Cancel changes":"Annuler les modifications","Change title":"Modifier le titre",Choose:"Choisir","Clear text":"Effacer le texte",Close:"Fermer","Close modal":"Fermer la fenêtre","Close navigation":"Fermer la navigation","Close sidebar":"Fermer la barre latérale","Confirm changes":"Confirmer les modifications",Custom:"Personnalisé","Edit item":"Éditer l'élément","Error getting related resources":"Erreur à la récupération des ressources liées","Error parsing svg":"Erreur d'analyse SVG","External documentation for {title}":"Documentation externe pour {title}",Favorite:"Favori",Flags:"Drapeaux","Food & Drink":"Nourriture & Boissons","Frequently used":"Utilisés fréquemment",Global:"Global","Go back to the list":"Retourner à la liste","Hide password":"Cacher le mot de passe","Message limit of {count} characters reached":"Limite de messages de {count} caractères atteinte","More items …":"Plus d'éléments...",Next:"Suivant","No emoji found":"Pas d’émoji trouvé","No results":"Aucun résultat",Objects:"Objets",Open:"Ouvrir",'Open link to "{resourceTitle}"':'Ouvrir le lien vers "{resourceTitle}"',"Open navigation":"Ouvrir la navigation","Password is secure":"Le mot de passe est sécurisé","Pause slideshow":"Mettre le diaporama en pause","People & Body":"Personnes & Corps","Pick an emoji":"Choisissez un émoji","Please select a time zone:":"Sélectionnez un fuseau horaire : ",Previous:"Précédent","Related resources":"Ressources liées",Search:"Chercher","Search results":"Résultats de recherche","Select a tag":"Sélectionnez une balise",Settings:"Paramètres","Settings navigation":"Navigation dans les paramètres","Show password":"Afficher le mot de passe","Smileys & Emotion":"Smileys & Émotions","Start slideshow":"Démarrer le diaporama",Submit:"Valider",Symbols:"Symboles","Travel & Places":"Voyage & Lieux","Type to search time zone":"Saisissez les premiers lettres pour rechercher un fuseau horaire","Unable to search the group":"Impossible de chercher le groupe","Undo changes":"Annuler les changements",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Écrire un message, utiliser "@" pour mentionner une personne, ":" pour l\'autocomplétion des émojis...'}},{locale:"gl",translations:{"{tag} (invisible)":"{tag} (invisíbel)","{tag} (restricted)":"{tag} (restrinxido)",Actions:"Accións",Activities:"Actividades","Animals & Nature":"Animais e natureza","Cancel changes":"Cancelar os cambios",Choose:"Escoller",Close:"Pechar","Confirm changes":"Confirma os cambios",Custom:"Personalizado","External documentation for {title}":"Documentación externa para {title}",Flags:"Bandeiras","Food & Drink":"Comida e bebida","Frequently used":"Usado con frecuencia","Message limit of {count} characters reached":"Acadouse o límite de {count} caracteres por mensaxe",Next:"Seguinte","No emoji found":"Non se atopou ningún «emoji»","No results":"Sen resultados",Objects:"Obxectos","Pause slideshow":"Pausar o diaporama","People & Body":"Persoas e corpo","Pick an emoji":"Escolla un «emoji»",Previous:"Anterir",Search:"Buscar","Search results":"Resultados da busca","Select a tag":"Seleccione unha etiqueta",Settings:"Axustes","Settings navigation":"Navegación polos axustes","Smileys & Emotion":"Sorrisos e emocións","Start slideshow":"Iniciar o diaporama",Submit:"Enviar",Symbols:"Símbolos","Travel & Places":"Viaxes e lugares","Unable to search the group":"Non foi posíbel buscar o grupo","Write message, @ to mention someone …":"Escriba a mensaxe, @ para mencionar a alguén…"}},{locale:"he",translations:{"{tag} (invisible)":"{tag} (נסתר)","{tag} (restricted)":"{tag} (מוגבל)",Actions:"פעולות",Activities:"פעילויות","Animals & Nature":"חיות וטבע",Choose:"בחירה",Close:"סגירה",Custom:"בהתאמה אישית",Flags:"דגלים","Food & Drink":"מזון ומשקאות","Frequently used":"בשימוש תדיר",Next:"הבא","No emoji found":"לא נמצא אמוג׳י","No results":"אין תוצאות",Objects:"חפצים","Pause slideshow":"השהיית מצגת","People & Body":"אנשים וגוף","Pick an emoji":"נא לבחור אמוג׳י",Previous:"הקודם",Search:"חיפוש","Search results":"תוצאות חיפוש","Select a tag":"בחירת תגית",Settings:"הגדרות","Smileys & Emotion":"חייכנים ורגשונים","Start slideshow":"התחלת המצגת",Symbols:"סמלים","Travel & Places":"טיולים ומקומות","Unable to search the group":"לא ניתן לחפש בקבוצה"}},{locale:"hu_HU",translations:{"{tag} (invisible)":"{tag} (láthatatlan)","{tag} (restricted)":"{tag} (korlátozott)",Actions:"Műveletek",Activities:"Tevékenységek","Animals & Nature":"Állatok és természet","Anything shared with the same group of people will show up here":"Minden, amit ugyanazzal a csoporttal oszt meg, itt fog megjelenni","Avatar of {displayName}":"{displayName} profilképe","Avatar of {displayName}, {status}":"{displayName} profilképe, {status}","Cancel changes":"Változtatások elvetése","Change title":"Cím megváltoztatása",Choose:"Válassszon","Clear text":"Szöveg törlése",Close:"Bezárás","Close modal":"Ablak bezárása","Close navigation":"Navigáció bezárása","Close sidebar":"Oldalsáv bezárása","Confirm changes":"Változtatások megerősítése",Custom:"Egyéni","Edit item":"Elem szerkesztése","Error getting related resources":"Hiba a kapcsolódó erőforrások lekérésekor","Error parsing svg":"Hiba az SVG feldolgozásakor","External documentation for {title}":"Külső dokumentáció ehhez: {title}",Favorite:"Kedvenc",Flags:"Zászlók","Food & Drink":"Étel és ital","Frequently used":"Gyakran használt",Global:"Globális","Go back to the list":"Ugrás vissza a listához","Hide password":"Jelszó elrejtése","Message limit of {count} characters reached":"{count} karakteres üzenetkorlát elérve","More items …":"További elemek...",Next:"Következő","No emoji found":"Nem található emodzsi","No results":"Nincs találat",Objects:"Tárgyak",Open:"Megnyitás",'Open link to "{resourceTitle}"':"A(z) „{resourceTitle}” hivatkozásának megnyitása","Open navigation":"Navigáció megnyitása","Password is secure":"A jelszó biztonságos","Pause slideshow":"Diavetítés szüneteltetése","People & Body":"Emberek és test","Pick an emoji":"Válasszon egy emodzsit","Please select a time zone:":"Válasszon időzónát:",Previous:"Előző","Related resources":"Kapcsolódó erőforrások",Search:"Keresés","Search results":"Találatok","Select a tag":"Válasszon címkét",Settings:"Beállítások","Settings navigation":"Navigáció a beállításokban","Show password":"Jelszó megjelenítése","Smileys & Emotion":"Mosolyok és érzelmek","Start slideshow":"Diavetítés indítása",Submit:"Beküldés",Symbols:"Szimbólumok","Travel & Places":"Utazás és helyek","Type to search time zone":"Gépeljen az időzóna kereséséhez","Unable to search the group":"A csoport nem kereshető","Undo changes":"Változtatások visszavonása",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':"Írjon egy üzenetet, használja a „@”-ot valaki megemlítéséhet, illetve a „:”-ot az emodzsik automatikus kiegészítéséhez…"}},{locale:"is",translations:{"{tag} (invisible)":"{tag} (ósýnilegt)","{tag} (restricted)":"{tag} (takmarkað)",Actions:"Aðgerðir",Activities:"Aðgerðir","Animals & Nature":"Dýr og náttúra",Choose:"Velja",Close:"Loka",Custom:"Sérsniðið",Flags:"Flögg","Food & Drink":"Matur og drykkur","Frequently used":"Oftast notað",Next:"Næsta","No emoji found":"Ekkert tjáningartákn fannst","No results":"Engar niðurstöður",Objects:"Hlutir","Pause slideshow":"Gera hlé á skyggnusýningu","People & Body":"Fólk og líkami","Pick an emoji":"Veldu tjáningartákn",Previous:"Fyrri",Search:"Leita","Search results":"Leitarniðurstöður","Select a tag":"Veldu merki",Settings:"Stillingar","Smileys & Emotion":"Broskallar og tilfinningar","Start slideshow":"Byrja skyggnusýningu",Symbols:"Tákn","Travel & Places":"Staðir og ferðalög","Unable to search the group":"Get ekki leitað í hópnum"}},{locale:"it",translations:{"{tag} (invisible)":"{tag} (invisibile)","{tag} (restricted)":"{tag} (limitato)",Actions:"Azioni",Activities:"Attività","Animals & Nature":"Animali e natura","Anything shared with the same group of people will show up here":"Tutto ciò che è stato condiviso con lo stesso gruppo di persone viene visualizzato qui","Avatar of {displayName}":"Avatar di {displayName}","Avatar of {displayName}, {status}":"Avatar di {displayName}, {status}","Cancel changes":"Annulla modifiche","Change title":"Modifica il titolo",Choose:"Scegli","Clear text":"Cancella il testo",Close:"Chiudi","Close modal":"Chiudi il messaggio modale","Close navigation":"Chiudi la navigazione","Close sidebar":"Chiudi la barra laterale","Confirm changes":"Conferma modifiche",Custom:"Personalizzato","Edit item":"Modifica l'elemento","Error getting related resources":"Errore nell'ottenere risorse correlate","Error parsing svg":"Errore nell'analizzare l'svg","External documentation for {title}":"Documentazione esterna per {title}",Favorite:"Preferito",Flags:"Bandiere","Food & Drink":"Cibo e bevande","Frequently used":"Usati di frequente",Global:"Globale","Go back to the list":"Torna all'elenco","Hide password":"Nascondi la password","Message limit of {count} characters reached":"Limite dei messaggi di {count} caratteri raggiunto","More items …":"Più elementi ...",Next:"Successivo","No emoji found":"Nessun emoji trovato","No results":"Nessun risultato",Objects:"Oggetti",Open:"Apri",'Open link to "{resourceTitle}"':'Apri il link a "{resourceTitle}"',"Open navigation":"Apri la navigazione","Password is secure":"La password è sicura","Pause slideshow":"Presentazione in pausa","People & Body":"Persone e corpo","Pick an emoji":"Scegli un emoji","Please select a time zone:":"Si prega di selezionare un fuso orario:",Previous:"Precedente","Related resources":"Risorse correlate",Search:"Cerca","Search results":"Risultati di ricerca","Select a tag":"Seleziona un'etichetta",Settings:"Impostazioni","Settings navigation":"Navigazione delle impostazioni","Show password":"Mostra la password","Smileys & Emotion":"Faccine ed emozioni","Start slideshow":"Avvia presentazione",Submit:"Invia",Symbols:"Simboli","Travel & Places":"Viaggi e luoghi","Type to search time zone":"Digita per cercare un fuso orario","Unable to search the group":"Impossibile cercare il gruppo","Undo changes":"Cancella i cambiamenti",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Scrivi un messaggio, "@" per menzionare qualcuno, ":" per il completamento automatico delle emoji ...'}},{locale:"ja_JP",translations:{"{tag} (invisible)":"{タグ} (不可視)","{tag} (restricted)":"{タグ} (制限付)",Actions:"操作",Activities:"アクティビティ","Animals & Nature":"動物と自然","Anything shared with the same group of people will show up here":"同じグループで共有しているものは、全てここに表示されます","Avatar of {displayName}":"{displayName} のアバター","Avatar of {displayName}, {status}":"{displayName}, {status} のアバター","Cancel changes":"変更をキャンセル","Change title":"タイトルを変更",Choose:"選択","Clear text":"テキストをクリア",Close:"閉じる","Close modal":"モーダルを閉じる","Close navigation":"ナビゲーションを閉じる","Close sidebar":"サイドバーを閉じる","Confirm changes":"変更を承認",Custom:"カスタム","Edit item":"編集","Error getting related resources":"関連リソースの取得エラー","Error parsing svg":"svgの解析エラー","External documentation for {title}":"{title} のための添付文書",Favorite:"お気に入り",Flags:"国旗","Food & Drink":"食べ物と飲み物","Frequently used":"よく使うもの",Global:"全体","Go back to the list":"リストに戻る","Hide password":"パスワードを非表示","Message limit of {count} characters reached":"{count} 文字のメッセージ上限に達しています","More items …":"他のアイテム",Next:"次","No emoji found":"絵文字が見つかりません","No results":"なし",Objects:"物",Open:"開く",'Open link to "{resourceTitle}"':'"{resourceTitle}"のリンクを開く',"Open navigation":"ナビゲーションを開く","Password is secure":"パスワードは保護されています","Pause slideshow":"スライドショーを一時停止","People & Body":"様々な人と体の部位","Pick an emoji":"絵文字を選択","Please select a time zone:":"タイムゾーンを選んで下さい:",Previous:"前","Related resources":"関連リソース",Search:"検索","Search results":"検索結果","Select a tag":"タグを選択",Settings:"設定","Settings navigation":"ナビゲーション設定","Show password":"パスワードを表示","Smileys & Emotion":"感情表現","Start slideshow":"スライドショーを開始",Submit:"提出",Symbols:"記号","Travel & Places":"旅行と場所","Type to search time zone":"タイムゾーン検索のため入力してください","Unable to search the group":"グループを検索できません","Undo changes":"変更を取り消し",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'メッセージを記入、"@"でメンション、":"で絵文字の自動補完 ...'}},{locale:"lt_LT",translations:{"{tag} (invisible)":"{tag} (nematoma)","{tag} (restricted)":"{tag} (apribota)",Actions:"Veiksmai",Activities:"Veiklos","Animals & Nature":"Gyvūnai ir gamta",Choose:"Pasirinkti",Close:"Užverti",Custom:"Tinkinti","External documentation for {title}":"Išorinė {title} dokumentacija",Flags:"Vėliavos","Food & Drink":"Maistas ir gėrimai","Frequently used":"Dažniausiai naudoti","Message limit of {count} characters reached":"Pasiekta {count} simbolių žinutės riba",Next:"Kitas","No emoji found":"Nerasta jaustukų","No results":"Nėra rezultatų",Objects:"Objektai","Pause slideshow":"Pristabdyti skaidrių rodymą","People & Body":"Žmonės ir kūnas","Pick an emoji":"Pasirinkti jaustuką",Previous:"Ankstesnis",Search:"Ieškoti","Search results":"Paieškos rezultatai","Select a tag":"Pasirinkti žymę",Settings:"Nustatymai","Settings navigation":"Naršymas nustatymuose","Smileys & Emotion":"Šypsenos ir emocijos","Start slideshow":"Pradėti skaidrių rodymą",Submit:"Pateikti",Symbols:"Simboliai","Travel & Places":"Kelionės ir vietos","Unable to search the group":"Nepavyko atlikti paiešką grupėje","Write message, @ to mention someone …":"Rašykite žinutę, naudokite @ norėdami kažką paminėti…"}},{locale:"lv",translations:{"{tag} (invisible)":"{tag} (neredzams)","{tag} (restricted)":"{tag} (ierobežots)",Choose:"Izvēlēties",Close:"Aizvērt",Next:"Nākamais","No results":"Nav rezultātu","Pause slideshow":"Pauzēt slaidrādi",Previous:"Iepriekšējais","Select a tag":"Izvēlēties birku",Settings:"Iestatījumi","Start slideshow":"Sākt slaidrādi"}},{locale:"mk",translations:{"{tag} (invisible)":"{tag} (невидливо)","{tag} (restricted)":"{tag} (ограничено)",Actions:"Акции",Activities:"Активности","Animals & Nature":"Животни & Природа","Avatar of {displayName}":"Аватар на {displayName}","Avatar of {displayName}, {status}":"Аватар на {displayName}, {status}","Cancel changes":"Откажи ги промените","Change title":"Промени наслов",Choose:"Избери",Close:"Затвори","Close modal":"Затвори модал","Close navigation":"Затвори навигација","Confirm changes":"Потврди ги промените",Custom:"Прилагодени","Edit item":"Уреди","External documentation for {title}":"Надворешна документација за {title}",Favorite:"Фаворити",Flags:"Знамиња","Food & Drink":"Храна & Пијалоци","Frequently used":"Најчесто користени",Global:"Глобално","Go back to the list":"Врати се на листата",items:"ставки","Message limit of {count} characters reached":"Ограничувањето на должината на пораката од {count} карактери е надминато","More {dashboardItemType} …":"Повеќе {dashboardItemType} …",Next:"Следно","No emoji found":"Не се пронајдени емотикони","No results":"Нема резултати",Objects:"Објекти",Open:"Отвори","Open navigation":"Отвори навигација","Pause slideshow":"Пузирај слајдшоу","People & Body":"Луѓе & Тело","Pick an emoji":"Избери емотикон","Please select a time zone:":"Изберете временска зона:",Previous:"Предходно",Search:"Барај","Search results":"Резултати од барувањето","Select a tag":"Избери ознака",Settings:"Параметри","Settings navigation":"Параметри за навигација","Smileys & Emotion":"Смешковци & Емотикони","Start slideshow":"Стартувај слајдшоу",Submit:"Испрати",Symbols:"Симболи","Travel & Places":"Патувања & Места","Type to search time zone":"Напишете за да пребарате временска зона","Unable to search the group":"Неможе да се принајде групата","Undo changes":"Врати ги промените","Write message, @ to mention someone, : for emoji autocompletion …":"Напиши порака, @ за да спомнете некого, : за емотинони автоатско комплетирање ..."}},{locale:"my",translations:{"{tag} (invisible)":"{tag} (ကွယ်ဝှက်ထား)","{tag} (restricted)":"{tag} (ကန့်သတ်)",Actions:"လုပ်ဆောင်ချက်များ",Activities:"ပြုလုပ်ဆောင်တာများ","Animals & Nature":"တိရစ္ဆာန်များနှင့် သဘာဝ","Avatar of {displayName}":"{displayName} ၏ ကိုယ်ပွား","Cancel changes":"ပြောင်းလဲမှုများ ပယ်ဖျက်ရန်",Choose:"ရွေးချယ်ရန်",Close:"ပိတ်ရန်","Confirm changes":"ပြောင်းလဲမှုများ အတည်ပြုရန်",Custom:"အလိုကျချိန်ညှိမှု","External documentation for {title}":"{title} အတွက် ပြင်ပ စာရွက်စာတမ်း",Flags:"အလံများ","Food & Drink":"အစားအသောက်","Frequently used":"မကြာခဏအသုံးပြုသော",Global:"ကမ္ဘာလုံးဆိုင်ရာ","Message limit of {count} characters reached":"ကန့်သတ် စာလုံးရေ {count} လုံး ပြည့်ပါပြီ",Next:"နောက်သို့ဆက်ရန်","No emoji found":"အီမိုဂျီ ရှာဖွေမတွေ့နိုင်ပါ","No results":"ရလဒ်မရှိပါ",Objects:"အရာဝတ္ထုများ","Pause slideshow":"စလိုက်ရှိုး ခေတ္တရပ်ရန်","People & Body":"လူပုဂ္ဂိုလ်များနှင့် ခန္ဓာကိုယ်","Pick an emoji":"အီမိုဂျီရွေးရန်","Please select a time zone:":"ဒေသစံတော်ချိန် ရွေးချယ်ပေးပါ",Previous:"ယခင်",Search:"ရှာဖွေရန်","Search results":"ရှာဖွေမှု ရလဒ်များ","Select a tag":"tag ရွေးချယ်ရန်",Settings:"ချိန်ညှိချက်များ","Settings navigation":"ချိန်ညှိချက်အညွှန်း","Smileys & Emotion":"စမိုင်လီများနှင့် အီမိုရှင်း","Start slideshow":"စလိုက်ရှိုးအား စတင်ရန်",Submit:"တင်သွင်းရန်",Symbols:"သင်္ကေတများ","Travel & Places":"ခရီးသွားလာခြင်းနှင့် နေရာများ","Type to search time zone":"ဒေသစံတော်ချိန်များ ရှာဖွေရန် စာရိုက်ပါ","Unable to search the group":"အဖွဲ့အား ရှာဖွေ၍ မရနိုင်ပါ","Write message, @ to mention someone …":"စာရေးသားရန်၊ တစ်စုံတစ်ဦးအား @ အသုံးပြု ရည်ညွှန်းရန်..."}},{locale:"nb_NO",translations:{"{tag} (invisible)":"{tag} (usynlig)","{tag} (restricted)":"{tag} (beskyttet)",Actions:"Handlinger",Activities:"Aktiviteter","Animals & Nature":"Dyr og natur","Anything shared with the same group of people will show up here":"Alt som er delt med den samme gruppen vil vises her","Avatar of {displayName}":"Avataren til {displayName}","Avatar of {displayName}, {status}":"{displayName}'s avatar, {status}","Cancel changes":"Avbryt endringer","Change title":"Endre tittel",Choose:"Velg","Clear text":"Fjern tekst",Close:"Lukk","Close modal":"Lukk modal","Close navigation":"Lukk navigasjon","Close sidebar":"Lukk sidepanel","Confirm changes":"Bekreft endringer",Custom:"Tilpasset","Edit item":"Rediger","Error getting related resources":"Feil ved henting av relaterte ressurser","Error parsing svg":"Feil ved parsing av svg","External documentation for {title}":"Ekstern dokumentasjon for {title}",Favorite:"Favoritt",Flags:"Flagg","Food & Drink":"Mat og drikke","Frequently used":"Ofte brukt",Global:"Global","Go back to the list":"Gå tilbake til listen","Hide password":"Skjul passord","Message limit of {count} characters reached":"Karakter begrensing {count} nådd i melding","More items …":"Flere gjenstander...",Next:"Neste","No emoji found":"Fant ingen emoji","No results":"Ingen resultater",Objects:"Objekter",Open:"Åpne",'Open link to "{resourceTitle}"':'Åpne link til "{resourceTitle}"',"Open navigation":"Åpne navigasjon","Password is secure":"Passordet er sikkert","Pause slideshow":"Pause lysbildefremvisning","People & Body":"Mennesker og kropp","Pick an emoji":"Velg en emoji","Please select a time zone:":"Vennligst velg tidssone",Previous:"Forrige","Related resources":"Relaterte ressurser",Search:"Søk","Search results":"Søkeresultater","Select a tag":"Velg en merkelapp",Settings:"Innstillinger","Settings navigation":"Navigasjonsinstillinger","Show password":"Vis passord","Smileys & Emotion":"Smilefjes og følelser","Start slideshow":"Start lysbildefremvisning",Submit:"Send",Symbols:"Symboler","Travel & Places":"Reise og steder","Type to search time zone":"Tast for å søke etter tidssone","Unable to search the group":"Kunne ikke søke i gruppen","Undo changes":"Tilbakestill endringer",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Skriv melding, bruk "@" for å nevne noen, bruk ":" for autofullføring av emoji...'}},{locale:"nl",translations:{"{tag} (invisible)":"{tag} (onzichtbaar)","{tag} (restricted)":"{tag} (beperkt)",Actions:"Acties",Activities:"Activiteiten","Animals & Nature":"Dieren & Natuur","Avatar of {displayName}":"Avatar van {displayName}","Avatar of {displayName}, {status}":"Avatar van {displayName}, {status}","Cancel changes":"Wijzigingen annuleren",Choose:"Kies",Close:"Sluiten","Close navigation":"Navigatie sluiten","Confirm changes":"Wijzigingen bevestigen",Custom:"Aangepast","Edit item":"Item bewerken","External documentation for {title}":"Externe documentatie voor {title}",Flags:"Vlaggen","Food & Drink":"Eten & Drinken","Frequently used":"Vaak gebruikt",Global:"Globaal","Go back to the list":"Ga terug naar de lijst","Message limit of {count} characters reached":"Berichtlimiet van {count} karakters bereikt",Next:"Volgende","No emoji found":"Geen emoji gevonden","No results":"Geen resultaten",Objects:"Objecten","Open navigation":"Navigatie openen","Pause slideshow":"Pauzeer diavoorstelling","People & Body":"Mensen & Lichaam","Pick an emoji":"Kies een emoji","Please select a time zone:":"Selecteer een tijdzone:",Previous:"Vorige",Search:"Zoeken","Search results":"Zoekresultaten","Select a tag":"Selecteer een label",Settings:"Instellingen","Settings navigation":"Instellingen navigatie","Smileys & Emotion":"Smileys & Emotie","Start slideshow":"Start diavoorstelling",Submit:"Verwerken",Symbols:"Symbolen","Travel & Places":"Reizen & Plaatsen","Type to search time zone":"Type om de tijdzone te zoeken","Unable to search the group":"Kan niet in de groep zoeken","Undo changes":"Wijzigingen ongedaan maken","Write message, @ to mention someone, : for emoji autocompletion …":"Schrijf bericht, @ om iemand te noemen, : voor emoji auto-aanvullen ..."}},{locale:"oc",translations:{"{tag} (invisible)":"{tag} (invisible)","{tag} (restricted)":"{tag} (limit)",Actions:"Accions",Choose:"Causir",Close:"Tampar",Next:"Seguent","No results":"Cap de resultat","Pause slideshow":"Metre en pausa lo diaporama",Previous:"Precedent","Select a tag":"Seleccionar una etiqueta",Settings:"Paramètres","Start slideshow":"Lançar lo diaporama"}},{locale:"pl",translations:{"{tag} (invisible)":"{tag} (niewidoczna)","{tag} (restricted)":"{tag} (ograniczona)",Actions:"Działania",Activities:"Aktywność","Animals & Nature":"Zwierzęta i natura","Anything shared with the same group of people will show up here":"Tutaj pojawi się wszystko, co zostało udostępnione tej samej grupie osób","Avatar of {displayName}":"Awatar {displayName}","Avatar of {displayName}, {status}":"Awatar {displayName}, {status}","Cancel changes":"Anuluj zmiany","Change title":"Zmień tytuł",Choose:"Wybierz","Clear text":"Wyczyść tekst",Close:"Zamknij","Close modal":"Zamknij modal","Close navigation":"Zamknij nawigację","Close sidebar":"Zamknij pasek boczny","Confirm changes":"Potwierdź zmiany",Custom:"Zwyczajne","Edit item":"Edytuj element","Error getting related resources":"Błąd podczas pobierania powiązanych zasobów","Error parsing svg":"Błąd podczas analizowania svg","External documentation for {title}":"Dokumentacja zewnętrzna dla {title}",Favorite:"Ulubiony",Flags:"Flagi","Food & Drink":"Jedzenie i picie","Frequently used":"Często używane",Global:"Globalnie","Go back to the list":"Powrót do listy","Hide password":"Ukryj hasło","Message limit of {count} characters reached":"Przekroczono limit wiadomości wynoszący {count} znaków","More items …":"Więcej pozycji…",Next:"Następny","No emoji found":"Nie znaleziono emoji","No results":"Brak wyników",Objects:"Obiekty",Open:"Otwórz",'Open link to "{resourceTitle}"':'Otwórz link do "{resourceTitle}"',"Open navigation":"Otwórz nawigację","Password is secure":"Hasło jest bezpieczne","Pause slideshow":"Wstrzymaj pokaz slajdów","People & Body":"Ludzie i ciało","Pick an emoji":"Wybierz emoji","Please select a time zone:":"Wybierz strefę czasową:",Previous:"Poprzedni","Related resources":"Powiązane zasoby",Search:"Szukaj","Search results":"Wyniki wyszukiwania","Select a tag":"Wybierz etykietę",Settings:"Ustawienia","Settings navigation":"Ustawienia nawigacji","Show password":"Pokaż hasło","Smileys & Emotion":"Buźki i emotikony","Start slideshow":"Rozpocznij pokaz slajdów",Submit:"Wyślij",Symbols:"Symbole","Travel & Places":"Podróże i miejsca","Type to search time zone":"Wpisz, aby wyszukać strefę czasową","Unable to search the group":"Nie można przeszukać grupy","Undo changes":"Cofnij zmiany",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Napisz wiadomość, "@" aby o kimś wspomnieć, ":" dla autouzupełniania emoji…'}},{locale:"pt_BR",translations:{"{tag} (invisible)":"{tag} (invisível)","{tag} (restricted)":"{tag} (restrito) ",Actions:"Ações",Activities:"Atividades","Animals & Nature":"Animais & Natureza","Anything shared with the same group of people will show up here":"Qualquer coisa compartilhada com o mesmo grupo de pessoas aparecerá aqui","Avatar of {displayName}":"Avatar de {displayName}","Avatar of {displayName}, {status}":"Avatar de {displayName}, {status}","Cancel changes":"Cancelar alterações","Change title":"Alterar título",Choose:"Escolher","Clear text":"Limpar texto",Close:"Fechar","Close modal":"Fechar modal","Close navigation":"Fechar navegação","Close sidebar":"Fechar barra lateral","Confirm changes":"Confirmar alterações",Custom:"Personalizado","Edit item":"Editar item","Error getting related resources":"Erro ao obter recursos relacionados","Error parsing svg":"Erro ao analisar svg","External documentation for {title}":"Documentação externa para {title}",Favorite:"Favorito",Flags:"Bandeiras","Food & Drink":"Comida & Bebida","Frequently used":"Mais usados",Global:"Global","Go back to the list":"Volte para a lista","Hide password":"Ocultar a senha","Message limit of {count} characters reached":"Limite de mensagem de {count} caracteres atingido","More items …":"Mais itens …",Next:"Próximo","No emoji found":"Nenhum emoji encontrado","No results":"Sem resultados",Objects:"Objetos",Open:"Aberto",'Open link to "{resourceTitle}"':'Abrir link para "{resourceTitle}"',"Open navigation":"Abrir navegação","Password is secure":"A senha é segura","Pause slideshow":"Pausar apresentação de slides","People & Body":"Pessoas & Corpo","Pick an emoji":"Escolha um emoji","Please select a time zone:":"Selecione um fuso horário: ",Previous:"Anterior","Related resources":"Recursos relacionados",Search:"Pesquisar","Search results":"Resultados da pesquisa","Select a tag":"Selecionar uma tag",Settings:"Configurações","Settings navigation":"Navegação de configurações","Show password":"Mostrar senha","Smileys & Emotion":"Smiles & Emoções","Start slideshow":"Iniciar apresentação de slides",Submit:"Enviar",Symbols:"Símbolo","Travel & Places":"Viagem & Lugares","Type to search time zone":"Digite para pesquisar o fuso horário ","Unable to search the group":"Não foi possível pesquisar o grupo","Undo changes":"Desfazer modificações",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Escreva mensagens, use "@" para mencionar algum, use ":" for autocompletar emoji …'}},{locale:"pt_PT",translations:{"{tag} (invisible)":"{tag} (invisivel)","{tag} (restricted)":"{tag} (restrito)",Actions:"Ações",Choose:"Escolher",Close:"Fechar",Next:"Seguinte","No results":"Sem resultados","Pause slideshow":"Pausar diaporama",Previous:"Anterior","Select a tag":"Selecionar uma etiqueta",Settings:"Definições","Start slideshow":"Iniciar diaporama","Unable to search the group":"Não é possível pesquisar o grupo"}},{locale:"ro",translations:{"{tag} (invisible)":"{tag} (invizibil)","{tag} (restricted)":"{tag} (restricționat)",Actions:"Acțiuni",Activities:"Activități","Animals & Nature":"Animale și natură","Anything shared with the same group of people will show up here":"Tot ceea ce este partajat cu același grup de persoane va fi afișat aici","Avatar of {displayName}":"Avatarul lui {displayName}","Avatar of {displayName}, {status}":"Avatarul lui {displayName}, {status}","Cancel changes":"Anulează modificările","Change title":"Modificați titlul",Choose:"Alegeți","Clear text":"Șterge textul",Close:"Închideți","Close modal":"Închideți modulul","Close navigation":"Închideți navigarea","Close sidebar":"Închide bara laterală","Confirm changes":"Confirmați modificările",Custom:"Personalizat","Edit item":"Editați elementul","Error getting related resources":" Eroare la returnarea resurselor legate","Error parsing svg":"Eroare de analizare a svg","External documentation for {title}":"Documentație externă pentru {title}",Favorite:"Favorit",Flags:"Marcaje","Food & Drink":"Alimente și băuturi","Frequently used":"Utilizate frecvent",Global:"Global","Go back to the list":"Întoarceți-vă la listă","Hide password":"Ascunde parola","Message limit of {count} characters reached":"Limita mesajului de {count} caractere a fost atinsă","More items …":"Mai multe articole ...",Next:"Următorul","No emoji found":"Nu s-a găsit niciun emoji","No results":"Nu există rezultate",Objects:"Obiecte",Open:"Deschideți",'Open link to "{resourceTitle}"':'Deschide legătura la "{resourceTitle}"',"Open navigation":"Deschideți navigația","Password is secure":"Parola este sigură","Pause slideshow":"Pauză prezentare de diapozitive","People & Body":"Oameni și corp","Pick an emoji":"Alege un emoji","Please select a time zone:":"Vă rugăm să selectați un fus orar:",Previous:"Anterior","Related resources":"Resurse legate",Search:"Căutare","Search results":"Rezultatele căutării","Select a tag":"Selectați o etichetă",Settings:"Setări","Settings navigation":"Navigare setări","Show password":"Arată parola","Smileys & Emotion":"Zâmbete și emoții","Start slideshow":"Începeți prezentarea de diapozitive",Submit:"Trimiteți",Symbols:"Simboluri","Travel & Places":"Călătorii și locuri","Type to search time zone":"Tastați pentru a căuta fusul orar","Unable to search the group":"Imposibilitatea de a căuta în grup","Undo changes":"Anularea modificărilor",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Scrie un mesaj, folosește "@" pentru a menționa pe cineva, folosește ":" pentru autocompletarea cu emoji ...'}},{locale:"ru",translations:{"{tag} (invisible)":"{tag} (невидимое)","{tag} (restricted)":"{tag} (ограниченное)",Actions:"Действия ",Activities:"События","Animals & Nature":"Животные и природа ","Avatar of {displayName}":"Аватар {displayName}","Avatar of {displayName}, {status}":"Фотография {displayName}, {status}","Cancel changes":"Отменить изменения",Choose:"Выберите",Close:"Закрыть","Close modal":"Закрыть модальное окно","Close navigation":"Закрыть навигацию","Confirm changes":"Подтвердить изменения",Custom:"Пользовательское","Edit item":"Изменить элемент","External documentation for {title}":"Внешняя документация для {title}",Flags:"Флаги","Food & Drink":"Еда, напиток","Frequently used":"Часто используемый",Global:"Глобальный","Go back to the list":"Вернуться к списку",items:"элементов","Message limit of {count} characters reached":"Достигнуто ограничение на количество символов в {count}","More {dashboardItemType} …":"Больше {dashboardItemType} …",Next:"Следующее","No emoji found":"Эмодзи не найдено","No results":"Результаты отсуствуют",Objects:"Объекты",Open:"Открыть","Open navigation":"Открыть навигацию","Pause slideshow":"Приостановить показ слйдов","People & Body":"Люди и тело","Pick an emoji":"Выберите эмодзи","Please select a time zone:":"Пожалуйста, выберите часовой пояс:",Previous:"Предыдущее",Search:"Поиск","Search results":"Результаты поиска","Select a tag":"Выберите метку",Settings:"Параметры","Settings navigation":"Навигация по настройкам","Smileys & Emotion":"Смайлики и эмоции","Start slideshow":"Начать показ слайдов",Submit:"Утвердить",Symbols:"Символы","Travel & Places":"Путешествия и места","Type to search time zone":"Введите для поиска часового пояса","Unable to search the group":"Невозможно найти группу","Undo changes":"Отменить изменения","Write message, @ to mention someone, : for emoji autocompletion …":"Напишите сообщение, @ - чтобы упомянуть кого-то, : - для автозаполнения эмодзи …"}},{locale:"sk_SK",translations:{"{tag} (invisible)":"{tag} (neviditeľný)","{tag} (restricted)":"{tag} (obmedzený)",Actions:"Akcie",Activities:"Aktivity","Animals & Nature":"Zvieratá a príroda","Avatar of {displayName}":"Avatar {displayName}","Avatar of {displayName}, {status}":"Avatar {displayName}, {status}","Cancel changes":"Zrušiť zmeny",Choose:"Vybrať",Close:"Zatvoriť","Close navigation":"Zavrieť navigáciu","Confirm changes":"Potvrdiť zmeny",Custom:"Zvyk","Edit item":"Upraviť položku","External documentation for {title}":"Externá dokumentácia pre {title}",Flags:"Vlajky","Food & Drink":"Jedlo a nápoje","Frequently used":"Často používané",Global:"Globálne","Go back to the list":"Naspäť na zoznam","Message limit of {count} characters reached":"Limit správy na {count} znakov dosiahnutý",Next:"Ďalší","No emoji found":"Nenašli sa žiadne emodži","No results":"Žiadne výsledky",Objects:"Objekty","Open navigation":"Otvoriť navigáciu","Pause slideshow":"Pozastaviť prezentáciu","People & Body":"Ľudia a telo","Pick an emoji":"Vyberte si emodži","Please select a time zone:":"Prosím vyberte časovú zónu:",Previous:"Predchádzajúci",Search:"Hľadať","Search results":"Výsledky vyhľadávania","Select a tag":"Vybrať štítok",Settings:"Nastavenia","Settings navigation":"Navigácia v nastaveniach","Smileys & Emotion":"Smajlíky a emócie","Start slideshow":"Začať prezentáciu",Submit:"Odoslať",Symbols:"Symboly","Travel & Places":"Cestovanie a miesta","Type to search time zone":"Začníte písať pre vyhľadávanie časovej zóny","Unable to search the group":"Skupinu sa nepodarilo nájsť","Undo changes":"Vrátiť zmeny","Write message, @ to mention someone, : for emoji autocompletion …":"Napíšte správu, @ ak chcete niekoho spomenúť, : pre automatické dopĺňanie emotikonov…"}},{locale:"sl",translations:{"{tag} (invisible)":"{tag} (nevidno)","{tag} (restricted)":"{tag} (omejeno)",Actions:"Dejanja",Activities:"Dejavnosti","Animals & Nature":"Živali in Narava","Avatar of {displayName}":"Podoba {displayName}","Avatar of {displayName}, {status}":"Prikazna slika {displayName}, {status}","Cancel changes":"Prekliči spremembe","Change title":"Spremeni naziv",Choose:"Izbor","Clear text":"Počisti besedilo",Close:"Zapri","Close modal":"Zapri pojavno okno","Close navigation":"Zapri krmarjenje","Close sidebar":"Zapri stransko vrstico","Confirm changes":"Potrdi spremembe",Custom:"Po meri","Edit item":"Uredi predmet","Error getting related resources":"Napaka pridobivanja povezanih virov","External documentation for {title}":"Zunanja dokumentacija za {title}",Favorite:"Priljubljeno",Flags:"Zastavice","Food & Drink":"Hrana in Pijača","Frequently used":"Pogostost uporabe",Global:"Splošno","Go back to the list":"Vrni se na seznam","Hide password":"Skrij geslo","Message limit of {count} characters reached":"Dosežena omejitev {count} znakov na sporočilo.","More items …":"Več predmetov ...",Next:"Naslednji","No emoji found":"Ni najdenih izraznih ikon","No results":"Ni zadetkov",Objects:"Predmeti",Open:"Odpri",'Open link to "{resourceTitle}"':"Odpri povezavo do »{resourceTitle}«","Open navigation":"Odpri krmarjenje","Password is secure":"Geslo je varno","Pause slideshow":"Ustavi predstavitev","People & Body":"Ljudje in Telo","Pick a date":"Izbor datuma","Pick a date and a time":"Izbor datuma in časa","Pick a month":"Izbor meseca","Pick a time":"Izbor časa","Pick a week":"Izbor tedna","Pick a year":"Izbor leta","Pick an emoji":"Izbor izrazne ikone","Please select a time zone:":"Izbor časovnega pasu:",Previous:"Predhodni","Related resources":"Povezani viri",Search:"Iskanje","Search results":"Zadetki iskanja","Select a tag":"Izbor oznake",Settings:"Nastavitve","Settings navigation":"Krmarjenje nastavitev","Show password":"Pokaži geslo","Smileys & Emotion":"Izrazne ikone","Start slideshow":"Začni predstavitev",Submit:"Pošlji",Symbols:"Simboli","Travel & Places":"Potovanja in Kraji","Type to search time zone":"Vpišite niz za iskanje časovnega pasu","Unable to search the group":"Ni mogoče iskati po skupini","Undo changes":"Razveljavi spremembe","Write message, @ to mention someone, : for emoji autocompletion …":"Napišite sporočilo, za omembo pred ime postavite@, začnite z : za vstavljanje izraznih ikon …"}},{locale:"sr",translations:{"{tag} (invisible)":"{tag} (nevidljivo)","{tag} (restricted)":"{tag} (ograničeno)",Actions:"Radnje",Activities:"Aktivnosti","Animals & Nature":"Životinje i Priroda","Avatar of {displayName}":"Avatar za {displayName}","Avatar of {displayName}, {status}":"Avatar za {displayName}, {status}","Cancel changes":"Otkaži izmene","Change title":"Izmeni naziv",Choose:"Изаберите",Close:"Затвори","Close modal":"Zatvori modal","Close navigation":"Zatvori navigaciju","Close sidebar":"Zatvori bočnu traku","Confirm changes":"Potvrdite promene",Custom:"Po meri","Edit item":"Uredi stavku","External documentation for {title}":"Eksterna dokumentacija za {title}",Favorite:"Omiljeni",Flags:"Zastave","Food & Drink":"Hrana i Piće","Frequently used":"Često korišćeno",Global:"Globalno","Go back to the list":"Natrag na listu",items:"stavke","Message limit of {count} characters reached":"Dostignuto je ograničenje za poruke od {count} znakova","More {dashboardItemType} …":"Više {dashboardItemType} …",Next:"Следеће","No emoji found":"Nije pronađen nijedan emodži","No results":"Нема резултата",Objects:"Objekti",Open:"Otvori","Open navigation":"Otvori navigaciju","Pause slideshow":"Паузирај слајд шоу","People & Body":"Ljudi i Telo","Pick an emoji":"Izaberi emodži","Please select a time zone:":"Molimo izaberite vremensku zonu:",Previous:"Претходно",Search:"Pretraži","Search results":"Rezultati pretrage","Select a tag":"Изаберите ознаку",Settings:"Поставке","Settings navigation":"Navigacija u podešavanjima","Smileys & Emotion":"Smajli i Emocije","Start slideshow":"Покрени слајд шоу",Submit:"Prihvati",Symbols:"Simboli","Travel & Places":"Putovanja i Mesta","Type to search time zone":"Ukucaj da pretražiš vremenske zone","Unable to search the group":"Nije moguće pretražiti grupu","Undo changes":"Poništi promene","Write message, @ to mention someone, : for emoji autocompletion …":"Napišite poruku, @ da pomenete nekoga, : za automatsko dovršavanje emodžija…"}},{locale:"sv",translations:{"{tag} (invisible)":"{tag} (osynlig)","{tag} (restricted)":"{tag} (begränsad)",Actions:"Åtgärder",Activities:"Aktiviteter","Animals & Nature":"Djur & Natur","Anything shared with the same group of people will show up here":"Något som delats med samma grupp av personer kommer att visas här","Avatar of {displayName}":"{displayName}s avatar","Avatar of {displayName}, {status}":"{displayName}s avatar, {status}","Cancel changes":"Avbryt ändringar","Change title":"Ändra titel",Choose:"Välj","Clear text":"Ta bort text",Close:"Stäng","Close modal":"Stäng modal","Close navigation":"Stäng navigering","Close sidebar":"Stäng sidopanel","Confirm changes":"Bekräfta ändringar",Custom:"Anpassad","Edit item":"Ändra","Error getting related resources":"Problem att hämta relaterade resurser","Error parsing svg":"Fel vid inläsning av svg","External documentation for {title}":"Extern dokumentation för {title}",Favorite:"Favorit",Flags:"Flaggor","Food & Drink":"Mat & Dryck","Frequently used":"Används ofta",Global:"Global","Go back to the list":"Gå tillbaka till listan","Hide password":"Göm lössenordet","Message limit of {count} characters reached":"Meddelandegräns {count} tecken används","More items …":"Fler objekt",Next:"Nästa","No emoji found":"Hittade inga emojis","No results":"Inga resultat",Objects:"Objekt",Open:"Öppna",'Open link to "{resourceTitle}"':'Öppna länk till "{resourceTitle}"',"Open navigation":"Öppna navigering","Password is secure":"Lössenordet är säkert","Pause slideshow":"Pausa bildspelet","People & Body":"Kropp & Själ","Pick an emoji":"Välj en emoji","Please select a time zone:":"Välj tidszon:",Previous:"Föregående","Related resources":"Relaterade resurser",Search:"Sök","Search results":"Sökresultat","Select a tag":"Välj en tag",Settings:"Inställningar","Settings navigation":"Inställningsmeny","Show password":"Visa lössenordet","Smileys & Emotion":"Selfies & Känslor","Start slideshow":"Starta bildspelet",Submit:"Skicka",Symbols:"Symboler","Travel & Places":"Resor & Sevärdigheter","Type to search time zone":"Skriv för att välja tidszon","Unable to search the group":"Kunde inte söka i gruppen","Undo changes":"Ångra ändringar",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'Skriv meddelande, använd "@" för att nämna någon, använd ":" för automatiska emojiförslag ...'}},{locale:"tr",translations:{"{tag} (invisible)":"{tag} (görünmez)","{tag} (restricted)":"{tag} (kısıtlı)",Actions:"İşlemler",Activities:"Etkinlikler","Animals & Nature":"Hayvanlar ve Doğa","Anything shared with the same group of people will show up here":"Aynı kişi grubu ile paylaşılan herşey burada görüntülenir","Avatar of {displayName}":"{displayName} avatarı","Avatar of {displayName}, {status}":"{displayName}, {status} avatarı","Cancel changes":"Değişiklikleri iptal et","Change title":"Başlığı değiştir",Choose:"Seçin","Clear text":"Metni temizle",Close:"Kapat","Close modal":"Üste açılan pencereyi kapat","Close navigation":"Gezinmeyi kapat","Close sidebar":"Yan çubuğu kapat","Confirm changes":"Değişiklikleri onayla",Custom:"Özel","Edit item":"Ögeyi düzenle","Error getting related resources":"İlgili kaynaklar alınırken sorun çıktı","Error parsing svg":"svg işlenirken sorun çıktı","External documentation for {title}":"{title} için dış belgeler",Favorite:"Sık kullanılanlara ekle",Flags:"Bayraklar","Food & Drink":"Yeme ve İçme","Frequently used":"Sık kullanılanlar",Global:"Evrensel","Go back to the list":"Listeye dön","Hide password":"Parolayı gizle","Message limit of {count} characters reached":"{count} karakter ileti sınırına ulaşıldı","More items …":"Diğer ögeler…",Next:"Sonraki","No emoji found":"Herhangi bir emoji bulunamadı","No results":"Herhangi bir sonuç bulunamadı",Objects:"Nesneler",Open:"Aç",'Open link to "{resourceTitle}"':'"{resourceTitle}" bağlantısını aç',"Open navigation":"Gezinmeyi aç","Password is secure":"Parola güvenli","Pause slideshow":"Slayt sunumunu duraklat","People & Body":"İnsanlar ve Beden","Pick an emoji":"Bir emoji seçin","Please select a time zone:":"Lütfen bir saat dilimi seçin:",Previous:"Önceki","Related resources":"İlgili kaynaklar",Search:"Arama","Search results":"Arama sonuçları","Select a tag":"Bir etiket seçin",Settings:"Ayarlar","Settings navigation":"Gezinme ayarları","Show password":"Parolayı görüntüle","Smileys & Emotion":"İfadeler ve Duygular","Start slideshow":"Slayt sunumunu başlat",Submit:"Gönder",Symbols:"Simgeler","Travel & Places":"Gezi ve Yerler","Type to search time zone":"Saat dilimi aramak için yazmaya başlayın","Unable to search the group":"Grupta arama yapılamadı","Undo changes":"Değişiklikleri geri al",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'İleti yazın, birini anmak için @, otomatik emoji tamamlamak için ":" kullanın…'}},{locale:"uk",translations:{"{tag} (invisible)":"{tag} (невидимий)","{tag} (restricted)":"{tag} (обмежений)",Actions:"Дії",Activities:"Діяльність","Animals & Nature":"Тварини та природа","Avatar of {displayName}":"Аватар {displayName}","Avatar of {displayName}, {status}":"Аватар {displayName}, {status}","Cancel changes":"Скасувати зміни","Change title":"Змінити назву",Choose:"ВиберітьВиберіть","Clear text":"Очистити текст",Close:"Закрити","Close modal":"Закрити модаль","Close navigation":"Закрити навігацію","Close sidebar":"Закрити бічну панель","Confirm changes":"Підтвердити зміни",Custom:"Власне","Edit item":"Редагувати елемент","External documentation for {title}":"Зовнішня документація для {title}",Favorite:"Улюблений",Flags:"Прапори","Food & Drink":"Їжа та напої","Frequently used":"Найчастіші",Global:"Глобальний","Go back to the list":"Повернутися до списку","Hide password":"Приховати пароль",items:"елементи","Message limit of {count} characters reached":"Вичерпано ліміт у {count} символів для повідомлення","More {dashboardItemType} …":"Більше {dashboardItemType}…",Next:"Вперед","No emoji found":"Емоційки відсутні","No results":"Відсутні результати",Objects:"Об'єкти",Open:"Відкрити","Open navigation":"Відкрити навігацію","Password is secure":"Пароль безпечний","Pause slideshow":"Пауза у показі слайдів","People & Body":"Люди та жести","Pick an emoji":"Виберіть емоційку","Please select a time zone:":"Виберіть часовий пояс:",Previous:"Назад",Search:"Пошук","Search results":"Результати пошуку","Select a tag":"Виберіть позначку",Settings:"Налаштування","Settings navigation":"Навігація у налаштуваннях","Show password":"Показати пароль","Smileys & Emotion":"Смайли та емоції","Start slideshow":"Почати показ слайдів",Submit:"Надіслати",Symbols:"Символи","Travel & Places":"Поїздки та місця","Type to search time zone":"Введіть для пошуку часовий пояс","Unable to search the group":"Неможливо шукати в групі","Undo changes":"Скасувати зміни","Write message, @ to mention someone, : for emoji autocompletion …":"Напишіть повідомлення, @, щоб згадати когось, : для автозаповнення емодзі…"}},{locale:"zh_CN",translations:{"{tag} (invisible)":"{tag} (不可见)","{tag} (restricted)":"{tag} (受限)",Actions:"行为",Activities:"活动","Animals & Nature":"动物 & 自然","Anything shared with the same group of people will show up here":"与同组用户分享的所有内容都会显示于此","Avatar of {displayName}":"{displayName}的头像","Avatar of {displayName}, {status}":"{displayName}的头像,{status}","Cancel changes":"取消更改","Change title":"更改标题",Choose:"选择","Clear text":"清除文本",Close:"关闭","Close modal":"关闭窗口","Close navigation":"关闭导航","Close sidebar":"关闭侧边栏","Confirm changes":"确认更改",Custom:"自定义","Edit item":"编辑项目","Error getting related resources":"获取相关资源时出错","Error parsing svg":"解析 svg 时出错","External documentation for {title}":"{title}的外部文档",Favorite:"喜爱",Flags:"旗帜","Food & Drink":"食物 & 饮品","Frequently used":"经常使用",Global:"全局","Go back to the list":"返回至列表","Hide password":"隐藏密码","Message limit of {count} characters reached":"已达到 {count} 个字符的消息限制","More items …":"更多项目…",Next:"下一个","No emoji found":"表情未找到","No results":"无结果",Objects:"物体",Open:"打开",'Open link to "{resourceTitle}"':'打开"{resourceTitle}"的连接',"Open navigation":"开启导航","Password is secure":"密码安全","Pause slideshow":"暂停幻灯片","People & Body":"人 & 身体","Pick an emoji":"选择一个表情","Please select a time zone:":"请选择一个时区:",Previous:"上一个","Related resources":"相关资源",Search:"搜索","Search results":"搜索结果","Select a tag":"选择一个标签",Settings:"设置","Settings navigation":"设置向导","Show password":"显示密码","Smileys & Emotion":"笑脸 & 情感","Start slideshow":"开始幻灯片",Submit:"提交",Symbols:"符号","Travel & Places":"旅游 & 地点","Type to search time zone":"打字以搜索时区","Unable to search the group":"无法搜索分组","Undo changes":"撤销更改",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'写信息,使用"@"来提及某人,使用":"进行表情符号自动完成 ...'}},{locale:"zh_HK",translations:{"{tag} (invisible)":"{tag} (隱藏)","{tag} (restricted)":"{tag} (受限)",Actions:"動作",Activities:"活動","Animals & Nature":"動物與自然","Anything shared with the same group of people will show up here":"與同一組人共享的任何內容都會顯示在此處","Avatar of {displayName}":"{displayName} 的頭像","Avatar of {displayName}, {status}":"{displayName} 的頭像,{status}","Cancel changes":"取消更改","Change title":"更改標題",Choose:"選擇","Clear text":"清除文本",Close:"關閉","Close modal":"關閉模態","Close navigation":"關閉導航","Close sidebar":"關閉側邊欄","Confirm changes":"確認更改",Custom:"自定義","Edit item":"編輯項目","Error getting related resources":"獲取相關資源出錯","Error parsing svg":"解析 svg 時出錯","External documentation for {title}":"{title} 的外部文檔",Favorite:"喜愛",Flags:"旗幟","Food & Drink":"食物與飲料","Frequently used":"經常使用",Global:"全球的","Go back to the list":"返回清單","Hide password":"隱藏密碼","Message limit of {count} characters reached":"已達到訊息最多 {count} 字元限制","More items …":"更多項目 …",Next:"下一個","No emoji found":"未找到表情符號","No results":"無結果",Objects:"物件",Open:"打開",'Open link to "{resourceTitle}"':"打開指向 “{resourceTitle}” 的鏈結","Open navigation":"開啟導航","Password is secure":"密碼是安全的","Pause slideshow":"暫停幻燈片","People & Body":"人物","Pick an emoji":"選擇表情符號","Please select a time zone:":"請選擇時區:",Previous:"上一個","Related resources":"相關資源",Search:"搜尋","Search results":"搜尋結果","Select a tag":"選擇標籤",Settings:"設定","Settings navigation":"設定值導覽","Show password":"顯示密碼","Smileys & Emotion":"表情","Start slideshow":"開始幻燈片",Submit:"提交",Symbols:"標誌","Travel & Places":"旅遊與景點","Type to search time zone":"鍵入以搜索時區","Unable to search the group":"無法搜尋群組","Undo changes":"取消更改",'Write message, use "@" to mention someone, use ":" for emoji autocompletion …':'寫訊息,使用 "@" 來指代某人,使用 ":" 用於表情符號自動填充 ...'}},{locale:"zh_TW",translations:{"{tag} (invisible)":"{tag} (隱藏)","{tag} (restricted)":"{tag} (受限)",Actions:"動作",Activities:"活動","Animals & Nature":"動物與自然",Choose:"選擇",Close:"關閉",Custom:"自定義",Flags:"旗幟","Food & Drink":"食物與飲料","Frequently used":"最近使用","Message limit of {count} characters reached":"已達到訊息最多 {count} 字元限制",Next:"下一個","No emoji found":"未找到表情符號","No results":"無結果",Objects:"物件","Pause slideshow":"暫停幻燈片","People & Body":"人物","Pick an emoji":"選擇表情符號",Previous:"上一個",Search:"搜尋","Search results":"搜尋結果","Select a tag":"選擇標籤",Settings:"設定","Settings navigation":"設定值導覽","Smileys & Emotion":"表情","Start slideshow":"開始幻燈片",Symbols:"標誌","Travel & Places":"旅遊與景點","Unable to search the group":"無法搜尋群組","Write message, @ to mention someone …":"輸入訊息時可使用 @ 來標示某人..."}}].forEach((function(e){var t={};for(var n in e.translations)e.translations[n].pluralId?t[n]={msgid:n,msgid_plural:e.translations[n].pluralId,msgstr:e.translations[n].msgstr}:t[n]={msgid:n,msgstr:[e.translations[n]]};r.addTranslation(e.locale,{translations:{"":t}})}));var o=r.build(),a=(o.ngettext.bind(o),o.gettext.bind(o))},1205:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const r=function(e){return Math.random().toString(36).replace(/[^a-z]+/g,"").slice(0,e||5)}},1390:(e,t,r)=>{"use strict";r.d(t,{Z:()=>i});const o=n(337);var a=r.n(o);const i=function(e){return a()(e,{defaultProtocol:"https",target:"_blank",className:"external linkified",attributes:{rel:"nofollow noopener noreferrer"}})}},1206:(e,t,n)=>{"use strict";n.d(t,{L:()=>r}),n(4505);var r=function(){return Object.assign(window,{_nc_focus_trap:window._nc_focus_trap||[]}),window._nc_focus_trap}},8384:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.v-popper--theme-tooltip.v-popper__popper{position:absolute;z-index:100000;top:0;right:auto;left:auto;display:block;margin:0;padding:0;text-align:left;text-align:start;opacity:0;line-height:1.6;line-break:auto;filter:drop-shadow(0 1px 10px var(--color-box-shadow))}.v-popper--theme-tooltip.v-popper__popper[data-popper-placement^=top] .v-popper__arrow-container{bottom:-10px;border-bottom-width:0;border-top-color:var(--color-main-background)}.v-popper--theme-tooltip.v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-container{top:-10px;border-top-width:0;border-bottom-color:var(--color-main-background)}.v-popper--theme-tooltip.v-popper__popper[data-popper-placement^=right] .v-popper__arrow-container{right:100%;border-left-width:0;border-right-color:var(--color-main-background)}.v-popper--theme-tooltip.v-popper__popper[data-popper-placement^=left] .v-popper__arrow-container{left:100%;border-right-width:0;border-left-color:var(--color-main-background)}.v-popper--theme-tooltip.v-popper__popper[aria-hidden=true]{visibility:hidden;transition:opacity .15s,visibility .15s;opacity:0}.v-popper--theme-tooltip.v-popper__popper[aria-hidden=false]{visibility:visible;transition:opacity .15s;opacity:1}.v-popper--theme-tooltip .v-popper__inner{max-width:350px;padding:5px 8px;text-align:center;color:var(--color-main-text);border-radius:var(--border-radius);background-color:var(--color-main-background)}.v-popper--theme-tooltip .v-popper__arrow-container{position:absolute;z-index:1;width:0;height:0;margin:0;border-style:solid;border-color:rgba(0,0,0,0);border-width:10px}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/directives/Tooltip/index.scss"],names:[],mappings:"AAGA,sBACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCSA,0CACC,iBAAA,CACA,cAAA,CACA,KAAA,CACA,UAAA,CACA,SAAA,CACA,aAAA,CACA,QAAA,CACA,SAAA,CACA,eAAA,CACA,gBAAA,CACA,SAAA,CACA,eAAA,CAEA,eAAA,CACA,sDAAA,CAGA,iGACC,YAAA,CACA,qBAAA,CACA,6CAAA,CAID,oGACC,SAAA,CACA,kBAAA,CACA,gDAAA,CAID,mGACC,UAAA,CACA,mBAAA,CACA,+CAAA,CAID,kGACC,SAAA,CACA,oBAAA,CACA,8CAAA,CAID,4DACC,iBAAA,CACA,uCAAA,CACA,SAAA,CAED,6DACC,kBAAA,CACA,uBAAA,CACA,SAAA,CAKF,0CACC,eAAA,CACA,eAAA,CACA,iBAAA,CACA,4BAAA,CACA,kCAAA,CACA,6CAAA,CAID,oDACC,iBAAA,CACA,SAAA,CACA,OAAA,CACA,QAAA,CACA,QAAA,CACA,kBAAA,CACA,0BAAA,CACA,iBAhFY",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n/**\n* @copyright Copyright (c) 2016, John Molakvoæ \n* @copyright Copyright (c) 2016, Robin Appelman \n* @copyright Copyright (c) 2016, Jan-Christoph Borchardt \n* @copyright Copyright (c) 2016, Erik Pellikka \n* @copyright Copyright (c) 2015, Vincent Petry \n*\n* Bootstrap (http://getbootstrap.com)\n* SCSS copied from version 3.3.5\n* Copyright 2011-2015 Twitter, Inc.\n* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n*/\n\n$arrow-width: 10px;\n\n.v-popper--theme-tooltip {\n\t&.v-popper__popper {\n\t\tposition: absolute;\n\t\tz-index: 100000;\n\t\ttop: 0;\n\t\tright: auto;\n\t\tleft: auto;\n\t\tdisplay: block;\n\t\tmargin: 0;\n\t\tpadding: 0;\n\t\ttext-align: left;\n\t\ttext-align: start;\n\t\topacity: 0;\n\t\tline-height: 1.6;\n\n\t\tline-break: auto;\n\t\tfilter: drop-shadow(0 1px 10px var(--color-box-shadow));\n\n\t\t// TOP\n\t\t&[data-popper-placement^='top'] .v-popper__arrow-container {\n\t\t\tbottom: -$arrow-width;\n\t\t\tborder-bottom-width: 0;\n\t\t\tborder-top-color: var(--color-main-background);\n\t\t}\n\n\t\t// BOTTOM\n\t\t&[data-popper-placement^='bottom'] .v-popper__arrow-container {\n\t\t\ttop: -$arrow-width;\n\t\t\tborder-top-width: 0;\n\t\t\tborder-bottom-color: var(--color-main-background);\n\t\t}\n\n\t\t// RIGHT\n\t\t&[data-popper-placement^='right'] .v-popper__arrow-container {\n\t\t\tright: 100%;\n\t\t\tborder-left-width: 0;\n\t\t\tborder-right-color: var(--color-main-background);\n\t\t}\n\n\t\t// LEFT\n\t\t&[data-popper-placement^='left'] .v-popper__arrow-container {\n\t\t\tleft: 100%;\n\t\t\tborder-right-width: 0;\n\t\t\tborder-left-color: var(--color-main-background);\n\t\t}\n\n\t\t// HIDDEN / SHOWN\n\t\t&[aria-hidden='true'] {\n\t\t\tvisibility: hidden;\n\t\t\ttransition: opacity .15s, visibility .15s;\n\t\t\topacity: 0;\n\t\t}\n\t\t&[aria-hidden='false'] {\n\t\t\tvisibility: visible;\n\t\t\ttransition: opacity .15s;\n\t\t\topacity: 1;\n\t\t}\n\t}\n\n\t// CONTENT\n\t.v-popper__inner {\n\t\tmax-width: 350px;\n\t\tpadding: 5px 8px;\n\t\ttext-align: center;\n\t\tcolor: var(--color-main-text);\n\t\tborder-radius: var(--border-radius);\n\t\tbackground-color: var(--color-main-background);\n\t}\n\n\t// ARROW\n\t.v-popper__arrow-container {\n\t\tposition: absolute;\n\t\tz-index: 1;\n\t\twidth: 0;\n\t\theight: 0;\n\t\tmargin: 0;\n\t\tborder-style: solid;\n\t\tborder-color: transparent;\n\t\tborder-width: $arrow-width;\n\t}\n}\n"],sourceRoot:""}]);const s=i},9286:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-3e2a42f5]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.action-items[data-v-3e2a42f5]{display:flex;align-items:center}.action-items>button[data-v-3e2a42f5]{margin-right:7px}.action-item[data-v-3e2a42f5]{--open-background-color: var(--color-background-hover, $action-background-hover);position:relative;display:inline-block}.action-item.action-item--primary[data-v-3e2a42f5]{--open-background-color: var(--color-primary-element-hover)}.action-item.action-item--secondary[data-v-3e2a42f5]{--open-background-color: var(--color-primary-element-light-hover)}.action-item.action-item--error[data-v-3e2a42f5]{--open-background-color: var(--color-error-hover)}.action-item.action-item--warning[data-v-3e2a42f5]{--open-background-color: var(--color-warning-hover)}.action-item.action-item--success[data-v-3e2a42f5]{--open-background-color: var(--color-success-hover)}.action-item.action-item--tertiary-no-background[data-v-3e2a42f5]{--open-background-color: transparent}.action-item.action-item--open .action-item__menutoggle[data-v-3e2a42f5]{background-color:var(--open-background-color)}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcActions/NcActions.vue"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCLD,+BACC,YAAA,CACA,kBAAA,CAGA,sCACC,gBAAA,CAIF,8BACC,gFAAA,CACA,iBAAA,CACA,oBAAA,CAEA,mDACC,2DAAA,CAGD,qDACC,iEAAA,CAGD,iDACC,iDAAA,CAGD,mDACC,mDAAA,CAGD,mDACC,mDAAA,CAGD,kEACC,oCAAA,CAGD,yEACC,6CAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n// Inline buttons\n.action-items {\n\tdisplay: flex;\n\talign-items: center;\n\n\t// Spacing between buttons\n\t& > button {\n\t\tmargin-right: math.div($icon-margin, 2);\n\t}\n}\n\n.action-item {\n\t--open-background-color: var(--color-background-hover, $action-background-hover);\n\tposition: relative;\n\tdisplay: inline-block;\n\n\t&.action-item--primary {\n\t\t--open-background-color: var(--color-primary-element-hover);\n\t}\n\n\t&.action-item--secondary {\n\t\t--open-background-color: var(--color-primary-element-light-hover);\n\t}\n\n\t&.action-item--error {\n\t\t--open-background-color: var(--color-error-hover);\n\t}\n\n\t&.action-item--warning {\n\t\t--open-background-color: var(--color-warning-hover);\n\t}\n\n\t&.action-item--success {\n\t\t--open-background-color: var(--color-success-hover);\n\t}\n\n\t&.action-item--tertiary-no-background {\n\t\t--open-background-color: transparent;\n\t}\n\n\t&.action-item--open .action-item__menutoggle {\n\t\tbackground-color: var(--open-background-color);\n\t}\n}\n"],sourceRoot:""}]);const s=i},2377:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper{border-radius:var(--border-radius-large);overflow:hidden}.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper .v-popper__inner{border-radius:var(--border-radius-large);padding:4px;max-height:calc(50vh - 16px);overflow:auto}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcActions/NcActions.vue"],names:[],mappings:"AAGA,sBACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCJD,kFACC,wCAAA,CACA,eAAA,CAEA,mGACC,wCAAA,CACA,WAAA,CACA,4BAAA,CACA,aAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n// We overwrote the popover base class, so we can style\n// the popover__inner for actions only.\n.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper {\n\tborder-radius: var(--border-radius-large);\n\toverflow:hidden;\n\n\t.v-popper__inner {\n\t\tborder-radius: var(--border-radius-large);\n\t\tpadding: 4px;\n\t\tmax-height: calc(50vh - 16px);\n\t\toverflow: auto;\n\t}\n}\n"],sourceRoot:""}]);const s=i},6801:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-62b02a03]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.app-sidebar[data-v-62b02a03]{z-index:1500;top:0;right:0;display:flex;overflow-x:hidden;overflow-y:auto;flex-direction:column;flex-shrink:0;width:27vw;min-width:300px;max-width:500px;height:100%;border-left:1px solid var(--color-border);background:var(--color-main-background)}.app-sidebar .app-sidebar-header>.app-sidebar__close[data-v-62b02a03]{position:absolute;z-index:100;top:6px;right:6px;width:44px;height:44px;opacity:.7;border-radius:22px}.app-sidebar .app-sidebar-header>.app-sidebar__close[data-v-62b02a03]:hover,.app-sidebar .app-sidebar-header>.app-sidebar__close[data-v-62b02a03]:active,.app-sidebar .app-sidebar-header>.app-sidebar__close[data-v-62b02a03]:focus{opacity:1;background-color:rgba(127,127,127,.25)}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info[data-v-62b02a03]{flex-direction:row}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info .app-sidebar-header__figure[data-v-62b02a03]{z-index:2;width:70px;height:70px;margin:9px;border-radius:3px;flex:0 0 auto}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info .app-sidebar-header__desc[data-v-62b02a03]{padding-left:0;flex:1 1 auto;min-width:0;padding-right:94px;padding-top:10px}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info .app-sidebar-header__desc.app-sidebar-header__desc--without-actions[data-v-62b02a03]{padding-right:50px}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info .app-sidebar-header__desc .app-sidebar-header__tertiary-actions[data-v-62b02a03]{z-index:3;position:absolute;top:9px;left:-44px;gap:0}.app-sidebar .app-sidebar-header--compact.app-sidebar-header--with-figure .app-sidebar-header__info .app-sidebar-header__desc .app-sidebar-header__menu[data-v-62b02a03]{top:6px;right:50px;background-color:rgba(0,0,0,0);position:absolute}.app-sidebar .app-sidebar-header:not(.app-sidebar-header--with-figure) .app-sidebar-header__menu[data-v-62b02a03]{position:absolute;top:6px;right:50px}.app-sidebar .app-sidebar-header:not(.app-sidebar-header--with-figure) .app-sidebar-header__desc[data-v-62b02a03]{padding-right:94px}.app-sidebar .app-sidebar-header:not(.app-sidebar-header--with-figure) .app-sidebar-header__desc.app-sidebar-header__desc--without-actions[data-v-62b02a03]{padding-right:50px}.app-sidebar .app-sidebar-header .app-sidebar-header__info[data-v-62b02a03]{display:flex;flex-direction:column}.app-sidebar .app-sidebar-header__figure[data-v-62b02a03]{width:100%;height:250px;max-height:250px;background-repeat:no-repeat;background-position:center;background-size:contain}.app-sidebar .app-sidebar-header__figure--with-action[data-v-62b02a03]{cursor:pointer}.app-sidebar .app-sidebar-header__desc[data-v-62b02a03]{position:relative;display:flex;flex-direction:row;justify-content:center;align-items:center;padding:18px 6px 18px 9px;gap:0 4px}.app-sidebar .app-sidebar-header__desc--with-tertiary-action[data-v-62b02a03]{padding-left:6px}.app-sidebar .app-sidebar-header__desc--editable .app-sidebar-header__maintitle-form[data-v-62b02a03],.app-sidebar .app-sidebar-header__desc--with-subtitle--editable .app-sidebar-header__maintitle-form[data-v-62b02a03]{margin-top:-2px;margin-bottom:-2px}.app-sidebar .app-sidebar-header__desc--with-subtitle--editable .app-sidebar-header__subtitle[data-v-62b02a03]{margin-top:-2px}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__tertiary-actions[data-v-62b02a03]{display:flex;height:44px;width:44px;justify-content:center;flex:0 0 auto}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__tertiary-actions .app-sidebar-header__star[data-v-62b02a03]{box-shadow:none}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__tertiary-actions .app-sidebar-header__star[data-v-62b02a03]:hover{box-shadow:none;background-color:var(--color-background-hover)}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container[data-v-62b02a03]{flex:1 1 auto;display:flex;flex-direction:column;justify-content:center;min-width:0}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container[data-v-62b02a03]{display:flex;align-items:center;min-height:44px}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container .app-sidebar-header__maintitle[data-v-62b02a03]{padding:0;min-height:30px;font-size:20px;line-height:30px}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container .app-sidebar-header__maintitle[data-v-62b02a03] .linkified{cursor:pointer;text-decoration:underline;margin:0}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container .app-sidebar-header__maintitle-form[data-v-62b02a03]{display:flex;flex:1 1 auto;align-items:center}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container .app-sidebar-header__maintitle-form input.app-sidebar-header__maintitle-input[data-v-62b02a03]{flex:1 1 auto;margin:0;padding:7px;font-size:20px;font-weight:bold}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle-container .app-sidebar-header__menu[data-v-62b02a03]{height:44px;width:44px;border-radius:22px;background-color:rgba(127,127,127,.25);margin-left:5px}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__maintitle[data-v-62b02a03],.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__subtitle[data-v-62b02a03]{overflow:hidden;width:100%;margin:0;white-space:nowrap;text-overflow:ellipsis}.app-sidebar .app-sidebar-header__desc .app-sidebar-header__title-container .app-sidebar-header__subtitle[data-v-62b02a03]{padding:0;opacity:.7;font-size:var(--default-font-size)}.app-sidebar .app-sidebar-header__description[data-v-62b02a03]{display:flex;align-items:center;margin:0 10px}@media only screen and (max-width: 768px){.app-sidebar[data-v-62b02a03]{width:100vw;max-width:100vw}}.slide-right-leave-active[data-v-62b02a03],.slide-right-enter-active[data-v-62b02a03]{transition-duration:var(--animation-quick);transition-property:max-width,min-width}.slide-right-enter-to[data-v-62b02a03],.slide-right-leave[data-v-62b02a03]{min-width:300px;max-width:500px}.slide-right-enter[data-v-62b02a03],.slide-right-leave-to[data-v-62b02a03]{min-width:0 !important;max-width:0 !important}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcAppSidebar/NcAppSidebar.vue","webpack://./src/assets/variables.scss"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCYD,8BACC,YAAA,CACA,KAAA,CACA,OAAA,CACA,YAAA,CACA,iBAAA,CACA,eAAA,CACA,qBAAA,CACA,aAAA,CACA,UAAA,CACA,eA5BmB,CA6BnB,eA5BmB,CA6BnB,WAAA,CACA,yCAAA,CACA,uCAAA,CAGC,sEACC,iBAAA,CACA,WAAA,CACA,OA1BmB,CA2BnB,SA3BmB,CA4BnB,UCjBc,CDkBd,WClBc,CDmBd,UCDc,CDEd,kBAAA,CACA,qOAGC,SCLW,CDMX,sCCFsB,CDQvB,qHACC,kBAAA,CAEA,iJACC,SAAA,CACA,UAAA,CACA,WAAA,CACA,UAAA,CACA,iBAAA,CACA,aAAA,CAED,+IACC,cAAA,CACA,aAAA,CACA,WAAA,CACA,kBAAA,CACA,gBAlE2B,CAoE3B,yLACC,kBAAA,CAGD,qLACC,SAAA,CACA,iBAAA,CACA,OAAA,CACA,UAAA,CACA,KAAA,CAED,yKACC,OAxEgB,CAyEhB,UAAA,CACA,8BAAA,CACA,iBAAA,CASH,kHACC,iBAAA,CACA,OAtFkB,CAuFlB,UAAA,CAGD,kHACC,kBAAA,CAEA,4JACC,kBAAA,CAMH,4EACC,YAAA,CACA,qBAAA,CAID,0DACC,UAAA,CACA,YAAA,CACA,gBAAA,CACA,2BAAA,CACA,0BAAA,CACA,uBAAA,CACA,uEACC,cAAA,CAKF,wDACC,iBAAA,CACA,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,kBAAA,CACA,yBAAA,CACA,SAAA,CAGA,8EACC,gBAAA,CAGD,2NAEC,eAAA,CACA,kBAAA,CAGD,+GACC,eAAA,CAGD,8FACC,YAAA,CACA,WCtIa,CDuIb,UCvIa,CDwIb,sBAAA,CACA,aAAA,CAEA,wHAEC,eAAA,CACA,8HACC,eAAA,CACA,8CAAA,CAMH,6FACC,aAAA,CACA,YAAA,CACA,qBAAA,CACA,sBAAA,CACA,WAAA,CAEA,sIACC,YAAA,CACA,kBAAA,CACA,eChKY,CDmKZ,qKACC,SAAA,CACA,eAAA,CACA,cAAA,CACA,gBAtLc,CAyLd,gLACC,cAAA,CACA,yBAAA,CACA,QAAA,CAIF,0KACC,YAAA,CACA,aAAA,CACA,kBAAA,CAEA,oNACC,aAAA,CACA,QAAA,CACA,WA3Mc,CA4Md,cAAA,CACA,gBAAA,CAKF,gKACC,WCjMW,CDkMX,UClMW,CDmMX,kBAAA,CACA,sCC7KoB,CD8KpB,eAAA,CAKF,uPAEC,eAAA,CACA,UAAA,CACA,QAAA,CACA,kBAAA,CACA,sBAAA,CAID,2HACC,SAAA,CACA,UCpMY,CDqMZ,kCAAA,CAMH,+DACC,YAAA,CACA,kBAAA,CACA,aAAA,CAMH,0CACC,8BACC,WAAA,CACA,eAAA,CAAA,CAIF,sFAEC,0CAAA,CACA,uCAAA,CAGD,2EAEC,eA5QmB,CA6QnB,eA5QmB,CA+QpB,2EAEC,sBAAA,CACA,sBAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n$sidebar-min-width: 300px;\n$sidebar-max-width: 500px;\n\n$desc-vertical-padding: 18px;\n$desc-vertical-padding-compact: 10px;\n$desc-input-padding: 7px;\n\n// title and subtitle\n$desc-title-height: 30px;\n$desc-subtitle-height: 22px;\n$desc-height: $desc-title-height + $desc-subtitle-height;\n\n$top-buttons-spacing: 6px;\n\n/*\n\tSidebar: to be used within #content\n\tapp-content will be shrinked properly\n*/\n.app-sidebar {\n\tz-index: 1500;\n\ttop: 0;\n\tright: 0;\n\tdisplay: flex;\n\toverflow-x: hidden;\n\toverflow-y: auto;\n\tflex-direction: column;\n\tflex-shrink: 0;\n\twidth: 27vw;\n\tmin-width: $sidebar-min-width;\n\tmax-width: $sidebar-max-width;\n\theight: 100%;\n\tborder-left: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n\n\t.app-sidebar-header {\n\t\t> .app-sidebar__close {\n\t\t\tposition: absolute;\n\t\t\tz-index: 100;\n\t\t\ttop: $top-buttons-spacing;\n\t\t\tright: $top-buttons-spacing;\n\t\t\twidth: $clickable-area;\n\t\t\theight: $clickable-area;\n\t\t\topacity: $opacity_normal;\n\t\t\tborder-radius: math.div($clickable-area, 2);\n\t\t\t&:hover,\n\t\t\t&:active,\n\t\t\t&:focus {\n\t\t\t\topacity: $opacity_full;\n\t\t\t\tbackground-color: $action-background-hover;\n\t\t\t}\n\t\t}\n\n\t\t// Compact mode only affects a sidebar with a figure\n\t\t&--compact.app-sidebar-header--with-figure {\n\t\t\t.app-sidebar-header__info {\n\t\t\t\tflex-direction: row;\n\n\t\t\t\t.app-sidebar-header__figure {\n\t\t\t\t\tz-index: 2;\n\t\t\t\t\twidth: $desc-height + $desc-vertical-padding;\n\t\t\t\t\theight: $desc-height + $desc-vertical-padding;\n\t\t\t\t\tmargin: math.div($desc-vertical-padding, 2);\n\t\t\t\t\tborder-radius: 3px;\n\t\t\t\t\tflex: 0 0 auto;\n\t\t\t\t}\n\t\t\t\t.app-sidebar-header__desc {\n\t\t\t\t\tpadding-left: 0;\n\t\t\t\t\tflex: 1 1 auto;\n\t\t\t\t\tmin-width: 0;\n\t\t\t\t\tpadding-right: 2 * $clickable-area + $top-buttons-spacing;\n\t\t\t\t\tpadding-top: $desc-vertical-padding-compact;\n\n\t\t\t\t\t&.app-sidebar-header__desc--without-actions {\n\t\t\t\t\t\tpadding-right: #{$clickable-area + $top-buttons-spacing};\n\t\t\t\t\t}\n\n\t\t\t\t\t.app-sidebar-header__tertiary-actions {\n\t\t\t\t\t\tz-index: 3; // above star\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\ttop: math.div($desc-vertical-padding, 2);\n\t\t\t\t\t\tleft: -1 * $clickable-area;\n\t\t\t\t\t\tgap: 0; // override gap\n\t\t\t\t\t}\n\t\t\t\t\t.app-sidebar-header__menu {\n\t\t\t\t\t\ttop: $top-buttons-spacing;\n\t\t\t\t\t\tright: $clickable-area + $top-buttons-spacing; // left of the close button\n\t\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// sidebar without figure\n\t\t&:not(.app-sidebar-header--with-figure) {\n\t\t\t// align the menu with the close button\n\t\t\t.app-sidebar-header__menu {\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: $top-buttons-spacing;\n\t\t\t\tright: $top-buttons-spacing + $clickable-area;\n\t\t\t}\n\t\t\t// increase the padding to not overlap the menu\n\t\t\t.app-sidebar-header__desc {\n\t\t\t\tpadding-right: #{$clickable-area * 2 + $top-buttons-spacing};\n\n\t\t\t\t&.app-sidebar-header__desc--without-actions {\n\t\t\t\t\tpadding-right: #{$clickable-area + $top-buttons-spacing};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// the container with the figure and the description\n\t\t.app-sidebar-header__info {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t}\n\n\t\t// header background\n\t\t&__figure {\n\t\t\twidth: 100%;\n\t\t\theight: 250px;\n\t\t\tmax-height: 250px;\n\t\t\tbackground-repeat: no-repeat;\n\t\t\tbackground-position: center;\n\t\t\tbackground-size: contain;\n\t\t\t&--with-action {\n\t\t\t\tcursor: pointer;\n\t\t\t}\n\t\t}\n\n\t\t// description\n\t\t&__desc {\n\t\t\tposition: relative;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tpadding: #{$desc-vertical-padding} #{$top-buttons-spacing} #{$desc-vertical-padding} #{math.div($desc-vertical-padding, 2)};\n\t\t\tgap: 0 4px;\n\n\t\t\t// custom overrides\n\t\t\t&--with-tertiary-action {\n\t\t\t\tpadding-left: 6px;\n\t\t\t}\n\n\t\t\t&--editable .app-sidebar-header__maintitle-form,\n\t\t\t&--with-subtitle--editable .app-sidebar-header__maintitle-form {\n\t\t\t\tmargin-top: -2px;\n\t\t\t\tmargin-bottom: -2px;\n\t\t\t}\n\n\t\t\t&--with-subtitle--editable .app-sidebar-header__subtitle {\n\t\t\t\tmargin-top: -2px;\n\t\t\t}\n\n\t\t\t.app-sidebar-header__tertiary-actions {\n\t\t\t\tdisplay: flex;\n\t\t\t\theight: $clickable-area;\n\t\t\t\twidth: $clickable-area;\n\t\t\t\tjustify-content: center;\n\t\t\t\tflex: 0 0 auto;\n\n\t\t\t\t.app-sidebar-header__star {\n\t\t\t\t\t// Override default Button component styles\n\t\t\t\t\tbox-shadow: none;\n\t\t\t\t\t&:hover {\n\t\t\t\t\t\tbox-shadow: none;\n\t\t\t\t\t\tbackground-color: var(--color-background-hover);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// titles\n\t\t\t.app-sidebar-header__title-container {\n\t\t\t\tflex: 1 1 auto;\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tjustify-content: center;\n\t\t\t\tmin-width: 0;\n\n\t\t\t\t.app-sidebar-header__maintitle-container {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tmin-height: $clickable-area;\n\n\t\t\t\t\t// main title\n\t\t\t\t\t.app-sidebar-header__maintitle {\n\t\t\t\t\t\tpadding: 0;\n\t\t\t\t\t\tmin-height: 30px;\n\t\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\t\tline-height: $desc-title-height;\n\n\t\t\t\t\t\t// Needs 'deep' as the link is generated by the linkify directive\n\t\t\t\t\t\t&:deep(.linkified) {\n\t\t\t\t\t\t\tcursor: pointer;\n\t\t\t\t\t\t\ttext-decoration: underline;\n\t\t\t\t\t\t\tmargin: 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t.app-sidebar-header__maintitle-form {\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tflex: 1 1 auto;\n\t\t\t\t\t\talign-items: center;\n\n\t\t\t\t\t\tinput.app-sidebar-header__maintitle-input {\n\t\t\t\t\t\t\tflex: 1 1 auto;\n\t\t\t\t\t\t\tmargin: 0;\n\t\t\t\t\t\t\tpadding: $desc-input-padding;\n\t\t\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// main menu\n\t\t\t\t\t.app-sidebar-header__menu {\n\t\t\t\t\t\theight: $clickable-area;\n\t\t\t\t\t\twidth: $clickable-area;\n\t\t\t\t\t\tborder-radius: math.div($clickable-area, 2);\n\t\t\t\t\t\tbackground-color: $action-background-hover;\n\t\t\t\t\t\tmargin-left: 5px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// shared between main and subtitle\n\t\t\t\t.app-sidebar-header__maintitle,\n\t\t\t\t.app-sidebar-header__subtitle {\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\twhite-space: nowrap;\n\t\t\t\t\ttext-overflow: ellipsis;\n\t\t\t\t}\n\n\t\t\t\t// subtitle\n\t\t\t\t.app-sidebar-header__subtitle {\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\topacity: $opacity_normal;\n\t\t\t\t\tfont-size: var(--default-font-size);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// sidebar description slot\n\t\t&__description {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tmargin: 0 10px;\n\t\t}\n\t}\n}\n\n// Make the sidebar full-width on small screens\n@media only screen and (max-width: 768px) {\n\t.app-sidebar {\n\t\twidth: 100vw;\n\t\tmax-width: 100vw;\n\t}\n}\n\n.slide-right-leave-active,\n.slide-right-enter-active {\n\ttransition-duration: var(--animation-quick);\n\ttransition-property: max-width, min-width;\n}\n\n.slide-right-enter-to,\n.slide-right-leave {\n\tmin-width: $sidebar-min-width;\n\tmax-width: $sidebar-max-width;\n}\n\n.slide-right-enter,\n.slide-right-leave-to {\n\tmin-width: 0 !important;\n\tmax-width: 0 !important;\n}\n","/**\n * @copyright Copyright (c) 2019 John Molakvoæ \n *\n * @author John Molakvoæ \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\n// https://uxplanet.org/7-rules-for-mobile-ui-button-design-e9cf2ea54556\n// recommended is 48px\n// 44px is what we choose and have very good visual-to-usability ratio\n$clickable-area: 44px;\n\n// background icon size\n// also used for the scss icon font\n$icon-size: 16px;\n\n// icon padding for a $clickable-area width and a $icon-size icon\n// ( 44px - 16px ) / 2\n$icon-margin: math.div($clickable-area - $icon-size, 2);\n\n// transparency background for icons\n$icon-focus-bg: rgba(127, 127, 127, .25);\n\n// popovermenu arrow width from the triangle center\n$arrow-width: 9px;\n\n// opacities\n$opacity_disabled: .5;\n$opacity_normal: .7;\n$opacity_full: 1;\n\n// menu round background hover feedback\n// good looking on dark AND white bg\n$action-background-hover: rgba(127, 127, 127, .25);\n\n// various structure data used in the \n// `AppNavigation` component\n$header-height: 50px;\n$navigation-width: 300px;\n\n// mobile breakpoint\n$breakpoint-mobile: 1024px;\n\n// top-bar spacing\n$topbar-margin: 4px;\n\n// navigation spacing\n$app-navigation-settings-margin: 3px;\n"],sourceRoot:""}]);const s=i},6180:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.app-sidebar-header__description button,.app-sidebar-header__description .button,.app-sidebar-header__description input[type=button],.app-sidebar-header__description input[type=submit],.app-sidebar-header__description input[type=reset]{padding:6px 22px}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcAppSidebar/NcAppSidebar.vue"],names:[],mappings:"AAGA,sBACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCHA,4OAIC,gBAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n// ! slots specific designs, cannot be scoped\n// if any button inside the description slot, increase visual padding\n.app-sidebar-header__description {\n\tbutton, .button,\n\tinput[type='button'],\n\tinput[type='submit'],\n\tinput[type='reset'] {\n\t\tpadding: 6px 22px;\n\t}\n}\n\n"],sourceRoot:""}]);const s=i},9290:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-204e1d5c]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.app-sidebar-tabs[data-v-204e1d5c]{display:flex;flex-direction:column;min-height:0;flex:1 1 100%}.app-sidebar-tabs__nav[data-v-204e1d5c]{margin-top:10px}.app-sidebar-tabs__nav ul[data-v-204e1d5c]{display:flex;justify-content:stretch}.app-sidebar-tabs__tab[data-v-204e1d5c]{display:block;flex:1 1;min-width:0;text-align:center}.app-sidebar-tabs__tab a[data-v-204e1d5c]{position:relative;display:block;overflow:hidden;padding:25px 5px 5px 5px;transition:color var(--animation-quick),opacity var(--animation-quick),border-color var(--animation-quick);text-align:center;white-space:nowrap;text-overflow:ellipsis;opacity:.7;color:var(--color-main-text);border-bottom:1px solid var(--color-border)}.app-sidebar-tabs__tab a[data-v-204e1d5c]:hover,.app-sidebar-tabs__tab a[data-v-204e1d5c]:focus,.app-sidebar-tabs__tab a[data-v-204e1d5c]:active,.app-sidebar-tabs__tab a.active[data-v-204e1d5c]{opacity:1}.app-sidebar-tabs__tab a:hover .app-sidebar-tabs__tab-icon[data-v-204e1d5c],.app-sidebar-tabs__tab a:focus .app-sidebar-tabs__tab-icon[data-v-204e1d5c],.app-sidebar-tabs__tab a:active .app-sidebar-tabs__tab-icon[data-v-204e1d5c],.app-sidebar-tabs__tab a.active .app-sidebar-tabs__tab-icon[data-v-204e1d5c]{opacity:1}.app-sidebar-tabs__tab a[data-v-204e1d5c]:not(.active):hover,.app-sidebar-tabs__tab a[data-v-204e1d5c]:not(.active):focus{border-bottom-color:var(--color-background-darker);box-shadow:inset 0 -1px 0 var(--color-background-darker)}.app-sidebar-tabs__tab a.active[data-v-204e1d5c]{color:var(--color-main-text);border-bottom-color:var(--color-main-text);box-shadow:inset 0 -1px 0 var(--color-main-text);font-weight:bold}.app-sidebar-tabs__tab a[data-v-204e1d5c]:focus{border-bottom-color:var(--color-primary-element);box-shadow:inset 0 -1px 0 var(--color-primary-element)}.app-sidebar-tabs__tab-icon[data-v-204e1d5c]{position:absolute;top:0;left:0;width:100%;height:25px;transition:opacity var(--animation-quick);opacity:.7}.app-sidebar-tabs__tab-icon>span[data-v-204e1d5c]{display:flex;align-items:center;justify-content:center;background-size:16px}.app-sidebar-tabs__content[data-v-204e1d5c]{position:relative;min-height:0;height:100%}.app-sidebar-tabs__content--multiple[data-v-204e1d5c]>:not(section){display:none}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcAppSidebar/NcAppSidebarTabs.vue","webpack://./src/assets/variables.scss"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCND,mCACC,YAAA,CACA,qBAAA,CACA,YAAA,CACA,aAAA,CAEA,wCACC,eAAA,CACA,2CACC,YAAA,CACA,uBAAA,CAGF,wCACC,aAAA,CACA,QAAA,CACA,WAAA,CACA,iBAAA,CACA,0CACC,iBAAA,CACA,aAAA,CACA,eAAA,CACA,wBAAA,CACA,0GAAA,CACA,iBAAA,CACA,kBAAA,CACA,sBAAA,CACA,UCcc,CDbd,4BAAA,CACA,2CAAA,CAEA,kMAIC,SCOW,CDNX,kTACC,SCKU,CDFZ,0HAEC,kDAAA,CACA,wDAAA,CAED,iDACC,4BAAA,CACA,0CAAA,CACA,gDAAA,CACA,gBAAA,CAKD,gDACC,gDAAA,CACA,sDAAA,CAKH,6CACC,iBAAA,CACA,KAAA,CACA,MAAA,CACA,UAAA,CACA,WAAA,CACA,yCAAA,CACA,UC3Be,CD6Bf,kDACC,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,oBAAA,CAIF,4CACC,iBAAA,CAEA,YAAA,CACA,WAAA,CAGA,oEACC,YAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n.app-sidebar-tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n\tflex: 1 1 100%;\n\n\t&__nav {\n\t\tmargin-top: 10px;\n\t\tul {\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: stretch;\n\t\t}\n\t}\n\t&__tab {\n\t\tdisplay: block;\n\t\tflex: 1 1;\n\t\tmin-width: 0;\n\t\ttext-align: center;\n\t\ta {\n\t\t\tposition: relative;\n\t\t\tdisplay: block;\n\t\t\toverflow: hidden;\n\t\t\tpadding: 25px 5px 5px 5px;\n\t\t\ttransition: color var(--animation-quick), opacity var(--animation-quick), border-color var(--animation-quick);\n\t\t\ttext-align: center;\n\t\t\twhite-space: nowrap;\n\t\t\ttext-overflow: ellipsis;\n\t\t\topacity: $opacity_normal;\n\t\t\tcolor: var(--color-main-text);\n\t\t\tborder-bottom: 1px solid var(--color-border);\n\n\t\t\t&:hover,\n\t\t\t&:focus,\n\t\t\t&:active,\n\t\t\t&.active {\n\t\t\t\topacity: $opacity_full;\n\t\t\t\t.app-sidebar-tabs__tab-icon {\n\t\t\t\t\topacity: $opacity_full;\n\t\t\t\t}\n\t\t\t}\n\t\t\t&:not(.active):hover,\n\t\t\t&:not(.active):focus {\n\t\t\t\tborder-bottom-color: var(--color-background-darker);\n\t\t\t\tbox-shadow: inset 0 -1px 0 var(--color-background-darker);\n\t\t\t}\n\t\t\t&.active {\n\t\t\t\tcolor: var(--color-main-text);\n\t\t\t\tborder-bottom-color: var(--color-main-text);\n\t\t\t\tbox-shadow: inset 0 -1px 0 var(--color-main-text);\n\t\t\t\tfont-weight: bold;\n\t\t\t}\n\t\t\t// differentiate the two for accessibility purpose\n\t\t\t// make sure the user knows she's focusing the navigation\n\t\t\t// and can use arrows/home/pageup...\n\t\t\t&:focus {\n\t\t\t\tborder-bottom-color: var(--color-primary-element);\n\t\t\t\tbox-shadow: inset 0 -1px 0 var(--color-primary-element);\n\t\t\t}\n\t\t}\n\t}\n\n\t&__tab-icon {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 25px;\n\t\ttransition: opacity var(--animation-quick);\n\t\topacity: $opacity_normal;\n\n\t\t& > span {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tbackground-size: 16px;\n\t\t}\n\t}\n\n\t&__content {\n\t\tposition: relative;\n\t\t// take full available height\n\t\tmin-height: 0;\n\t\theight: 100%;\n\t\t// force the use of the tab component if more than one tab\n\t\t// you can just put raw content if you don't use tabs\n\t\t&--multiple > :not(section) {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n","/**\n * @copyright Copyright (c) 2019 John Molakvoæ \n *\n * @author John Molakvoæ \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\n// https://uxplanet.org/7-rules-for-mobile-ui-button-design-e9cf2ea54556\n// recommended is 48px\n// 44px is what we choose and have very good visual-to-usability ratio\n$clickable-area: 44px;\n\n// background icon size\n// also used for the scss icon font\n$icon-size: 16px;\n\n// icon padding for a $clickable-area width and a $icon-size icon\n// ( 44px - 16px ) / 2\n$icon-margin: math.div($clickable-area - $icon-size, 2);\n\n// transparency background for icons\n$icon-focus-bg: rgba(127, 127, 127, .25);\n\n// popovermenu arrow width from the triangle center\n$arrow-width: 9px;\n\n// opacities\n$opacity_disabled: .5;\n$opacity_normal: .7;\n$opacity_full: 1;\n\n// menu round background hover feedback\n// good looking on dark AND white bg\n$action-background-hover: rgba(127, 127, 127, .25);\n\n// various structure data used in the \n// `AppNavigation` component\n$header-height: 50px;\n$navigation-width: 300px;\n\n// mobile breakpoint\n$breakpoint-mobile: 1024px;\n\n// top-bar spacing\n$topbar-margin: 4px;\n\n// navigation spacing\n$app-navigation-settings-margin: 3px;\n"],sourceRoot:""}]);const s=i},1898:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-2e49be1e]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.button-vue[data-v-2e49be1e]{position:relative;width:fit-content;overflow:hidden;border:0;padding:0;font-size:var(--default-font-size);font-weight:bold;min-height:44px;min-width:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;border-radius:22px;transition-property:color,border-color,background-color;transition-duration:.1s;transition-timing-function:linear;color:var(--color-primary-element-light-text);background-color:var(--color-primary-element-light)}.button-vue *[data-v-2e49be1e],.button-vue span[data-v-2e49be1e]{cursor:pointer}.button-vue[data-v-2e49be1e]:focus{outline:none}.button-vue[data-v-2e49be1e]:disabled{cursor:default;opacity:.5;filter:saturate(0.7)}.button-vue:disabled *[data-v-2e49be1e]{cursor:default}.button-vue[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color-primary-element-light-hover)}.button-vue[data-v-2e49be1e]:active{background-color:var(--color-primary-element-light)}.button-vue__wrapper[data-v-2e49be1e]{display:inline-flex;align-items:center;justify-content:center;width:100%}.button-vue__icon[data-v-2e49be1e]{height:44px;width:44px;min-height:44px;min-width:44px;display:flex;justify-content:center;align-items:center}.button-vue__text[data-v-2e49be1e]{font-weight:bold;margin-bottom:1px;padding:2px 0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.button-vue--icon-only[data-v-2e49be1e]{width:44px !important}.button-vue--text-only[data-v-2e49be1e]{padding:0 12px}.button-vue--text-only .button-vue__text[data-v-2e49be1e]{margin-left:4px;margin-right:4px}.button-vue--icon-and-text[data-v-2e49be1e]{padding:0 16px 0 4px}.button-vue--wide[data-v-2e49be1e]{width:100%}.button-vue[data-v-2e49be1e]:focus-visible{outline:2px solid var(--color-main-text) !important}.button-vue:focus-visible.button-vue--vue-tertiary-on-primary[data-v-2e49be1e]{outline:2px solid var(--color-primary-element-text);border-radius:var(--border-radius);background-color:rgba(0,0,0,0)}.button-vue--vue-primary[data-v-2e49be1e]{background-color:var(--color-primary-element);color:var(--color-primary-element-text)}.button-vue--vue-primary[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color-primary-element-hover)}.button-vue--vue-primary[data-v-2e49be1e]:active{background-color:var(--color-primary-element)}.button-vue--vue-secondary[data-v-2e49be1e]{color:var(--color-primary-element-light-text);background-color:var(--color-primary-element-light)}.button-vue--vue-secondary[data-v-2e49be1e]:hover:not(:disabled){color:var(--color-primary-element-light-text);background-color:var(--color-primary-element-light-hover)}.button-vue--vue-tertiary[data-v-2e49be1e]{color:var(--color-main-text);background-color:rgba(0,0,0,0)}.button-vue--vue-tertiary[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color);background-color:var(--color-background-hover)}.button-vue--vue-tertiary-no-background[data-v-2e49be1e]{color:var(--color-main-text);background-color:rgba(0,0,0,0)}.button-vue--vue-tertiary-no-background[data-v-2e49be1e]:hover:not(:disabled){background-color:rgba(0,0,0,0)}.button-vue--vue-tertiary-on-primary[data-v-2e49be1e]{color:var(--color-primary-element-text);background-color:rgba(0,0,0,0)}.button-vue--vue-tertiary-on-primary[data-v-2e49be1e]:hover:not(:disabled){background-color:rgba(0,0,0,0)}.button-vue--vue-success[data-v-2e49be1e]{background-color:var(--color-success);color:#fff}.button-vue--vue-success[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color-success-hover)}.button-vue--vue-success[data-v-2e49be1e]:active{background-color:var(--color-success)}.button-vue--vue-warning[data-v-2e49be1e]{background-color:var(--color-warning);color:#fff}.button-vue--vue-warning[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color-warning-hover)}.button-vue--vue-warning[data-v-2e49be1e]:active{background-color:var(--color-warning)}.button-vue--vue-error[data-v-2e49be1e]{background-color:var(--color-error);color:#fff}.button-vue--vue-error[data-v-2e49be1e]:hover:not(:disabled){background-color:var(--color-error-hover)}.button-vue--vue-error[data-v-2e49be1e]:active{background-color:var(--color-error)}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcButton/NcButton.vue","webpack://./src/assets/variables.scss"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCLD,6BACC,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,QAAA,CACA,SAAA,CACA,kCAAA,CACA,gBAAA,CACA,eCcgB,CDbhB,cCagB,CDZhB,YAAA,CACA,kBAAA,CACA,sBAAA,CAGA,cAAA,CAKA,kBAAA,CACA,uDAAA,CACA,uBAAA,CACA,iCAAA,CAkBA,6CAAA,CACA,mDAAA,CA1BA,iEAEC,cAAA,CAQD,mCACC,YAAA,CAGD,sCACC,cAAA,CAIA,UCIiB,CDFjB,oBAAA,CALA,wCACC,cAAA,CAUF,kDACC,yDAAA,CAKD,oCACC,mDAAA,CAGD,sCACC,mBAAA,CACA,kBAAA,CACA,sBAAA,CACA,UAAA,CAGD,mCACC,WCvCe,CDwCf,UCxCe,CDyCf,eCzCe,CD0Cf,cC1Ce,CD2Cf,YAAA,CACA,sBAAA,CACA,kBAAA,CAGD,mCACC,gBAAA,CACA,iBAAA,CACA,aAAA,CACA,kBAAA,CACA,sBAAA,CACA,eAAA,CAID,wCACC,qBAAA,CAID,wCACC,cAAA,CACA,0DACC,eAAA,CACA,gBAAA,CAKF,4CACC,oBAAA,CAID,mCACC,UAAA,CAGD,2CACC,mDAAA,CACA,+EACC,mDAAA,CACA,kCAAA,CACA,8BAAA,CAOF,0CACC,6CAAA,CACA,uCAAA,CACA,+DACC,mDAAA,CAID,iDACC,6CAAA,CAKF,4CACC,6CAAA,CACA,mDAAA,CACA,iEACC,6CAAA,CACA,yDAAA,CAKF,2CACC,4BAAA,CACA,8BAAA,CACA,gEACC,6BAAA,CACA,8CAAA,CAKF,yDACC,4BAAA,CACA,8BAAA,CACA,8EACC,8BAAA,CAKF,sDACC,uCAAA,CACA,8BAAA,CAEA,2EACC,8BAAA,CAKF,0CACC,qCAAA,CACA,UAAA,CACA,+DACC,2CAAA,CAID,iDACC,qCAAA,CAKF,0CACC,qCAAA,CACA,UAAA,CACA,+DACC,2CAAA,CAID,iDACC,qCAAA,CAKF,wCACC,mCAAA,CACA,UAAA,CACA,6DACC,yCAAA,CAID,+CACC,mCAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n\n.button-vue {\n\tposition: relative;\n\twidth: fit-content;\n\toverflow: hidden;\n\tborder: 0;\n\tpadding: 0;\n\tfont-size: var(--default-font-size);\n\tfont-weight: bold;\n\tmin-height: $clickable-area;\n\tmin-width: $clickable-area;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\n\t// Cursor pointer on element and all children\n\tcursor: pointer;\n\t& *,\n\tspan {\n\t\tcursor: pointer;\n\t}\n\tborder-radius: math.div($clickable-area, 2);\n\ttransition-property: color, border-color, background-color;\n\ttransition-duration: 0.1s;\n\ttransition-timing-function: linear;\n\n\t// No outline feedback for focus. Handled with a toggled class in js (see data)\n\t&:focus {\n\t\toutline: none;\n\t}\n\n\t&:disabled {\n\t\tcursor: default;\n\t\t& * {\n\t\t\tcursor: default;\n\t\t}\n\t\topacity: $opacity_disabled;\n\t\t// Gives a wash out effect\n\t\tfilter: saturate($opacity_normal);\n\t}\n\n\t// Default button type\n\tcolor: var(--color-primary-element-light-text);\n\tbackground-color: var(--color-primary-element-light);\n\t&:hover:not(:disabled) {\n\t\tbackground-color: var(--color-primary-element-light-hover);\n\t}\n\n\t// Back to the default color for this button when active\n\t// TODO: add ripple effect\n\t&:active {\n\t\tbackground-color: var(--color-primary-element-light);\n\t}\n\n\t&__wrapper {\n\t\tdisplay: inline-flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t}\n\n\t&__icon {\n\t\theight: $clickable-area;\n\t\twidth: $clickable-area;\n\t\tmin-height: $clickable-area;\n\t\tmin-width: $clickable-area;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t}\n\n\t&__text {\n\t\tfont-weight: bold;\n\t\tmargin-bottom: 1px;\n\t\tpadding: 2px 0;\n\t\twhite-space: nowrap;\n\t\ttext-overflow: ellipsis;\n\t\toverflow: hidden;\n\t}\n\n\t// Icon-only button\n\t&--icon-only {\n\t\twidth: $clickable-area !important;\n\t}\n\n\t// Text-only button\n\t&--text-only {\n\t\tpadding: 0 12px;\n\t\t& .button-vue__text {\n\t\t\tmargin-left: 4px;\n\t\t\tmargin-right: 4px;\n\t\t}\n\t}\n\n\t// Icon and text button\n\t&--icon-and-text {\n\t\tpadding: 0 16px 0 4px;\n\t}\n\n\t// Wide button spans the whole width of the container\n\t&--wide {\n\t\twidth: 100%;\n\t}\n\n\t&:focus-visible {\n\t\toutline: 2px solid var(--color-main-text) !important;\n\t\t&.button-vue--vue-tertiary-on-primary {\n\t\t\toutline: 2px solid var(--color-primary-element-text);\n\t\t\tborder-radius: var(--border-radius);\n\t\t\tbackground-color: transparent;\n\t\t}\n\t}\n\n\t// Button types\n\n\t// Primary\n\t&--vue-primary {\n\t\tbackground-color: var(--color-primary-element);\n\t\tcolor: var(--color-primary-element-text);\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: var(--color-primary-element-hover);\n\t\t}\n\t\t// Back to the default color for this button when active\n\t\t// TODO: add ripple effect\n\t\t&:active {\n\t\t\tbackground-color: var(--color-primary-element);\n\t\t}\n\t}\n\n\t// Secondary\n\t&--vue-secondary {\n\t\tcolor: var(--color-primary-element-light-text);\n\t\tbackground-color: var(--color-primary-element-light);\n\t\t&:hover:not(:disabled) {\n\t\t\tcolor: var(--color-primary-element-light-text);\n\t\t\tbackground-color: var(--color-primary-element-light-hover);\n\t\t}\n\t}\n\n\t// Tertiary\n\t&--vue-tertiary {\n\t\tcolor: var(--color-main-text);\n\t\tbackground-color: transparent;\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: var(--color);\n\t\t\tbackground-color: var(--color-background-hover);\n\t\t}\n\t}\n\n\t// Tertiary, no background\n\t&--vue-tertiary-no-background {\n\t\tcolor: var(--color-main-text);\n\t\tbackground-color: transparent;\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: transparent;\n\t\t}\n\t}\n\n\t// Tertiary on primary color (like the header)\n\t&--vue-tertiary-on-primary {\n\t\tcolor: var(--color-primary-element-text);\n\t\tbackground-color: transparent;\n\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: transparent;\n\t\t}\n\t}\n\n\t// Success\n\t&--vue-success {\n\t\tbackground-color: var(--color-success);\n\t\tcolor: white;\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: var(--color-success-hover);\n\t\t}\n\t\t// Back to the default color for this button when active\n\t\t// : add ripple effect\n\t\t&:active {\n\t\t\tbackground-color: var(--color-success);\n\t\t}\n\t}\n\n\t// Warning\n\t&--vue-warning {\n\t\tbackground-color: var(--color-warning);\n\t\tcolor: white;\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: var(--color-warning-hover);\n\t\t}\n\t\t// Back to the default color for this button when active\n\t\t// TODO: add ripple effect\n\t\t&:active {\n\t\t\tbackground-color: var(--color-warning);\n\t\t}\n\t}\n\n\t// Error\n\t&--vue-error {\n\t\tbackground-color: var(--color-error);\n\t\tcolor: white;\n\t\t&:hover:not(:disabled) {\n\t\t\tbackground-color: var(--color-error-hover);\n\t\t}\n\t\t// Back to the default color for this button when active\n\t\t// TODO: add ripple effect\n\t\t&:active {\n\t\t\tbackground-color: var(--color-error);\n\t\t}\n\t}\n}\n\n","/**\n * @copyright Copyright (c) 2019 John Molakvoæ \n *\n * @author John Molakvoæ \n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see .\n *\n */\n\n// https://uxplanet.org/7-rules-for-mobile-ui-button-design-e9cf2ea54556\n// recommended is 48px\n// 44px is what we choose and have very good visual-to-usability ratio\n$clickable-area: 44px;\n\n// background icon size\n// also used for the scss icon font\n$icon-size: 16px;\n\n// icon padding for a $clickable-area width and a $icon-size icon\n// ( 44px - 16px ) / 2\n$icon-margin: math.div($clickable-area - $icon-size, 2);\n\n// transparency background for icons\n$icon-focus-bg: rgba(127, 127, 127, .25);\n\n// popovermenu arrow width from the triangle center\n$arrow-width: 9px;\n\n// opacities\n$opacity_disabled: .5;\n$opacity_normal: .7;\n$opacity_full: 1;\n\n// menu round background hover feedback\n// good looking on dark AND white bg\n$action-background-hover: rgba(127, 127, 127, .25);\n\n// various structure data used in the \n// `AppNavigation` component\n$header-height: 50px;\n$navigation-width: 300px;\n\n// mobile breakpoint\n$breakpoint-mobile: 1024px;\n\n// top-bar spacing\n$topbar-margin: 4px;\n\n// navigation spacing\n$app-navigation-settings-margin: 3px;\n"],sourceRoot:""}]);const s=i},3300:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-04d732c3]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.empty-content[data-v-04d732c3]{display:flex;align-items:center;flex-direction:column;margin-top:20vh}.modal-wrapper .empty-content[data-v-04d732c3]{margin-top:5vh;margin-bottom:5vh}.empty-content__icon[data-v-04d732c3]{display:flex;align-items:center;justify-content:center;width:64px;height:64px;margin:0 auto 15px;opacity:.4;background-repeat:no-repeat;background-position:center;background-size:64px}.empty-content__icon[data-v-04d732c3] svg{width:64px;height:64px}.empty-content__title[data-v-04d732c3]{margin-bottom:10px;text-align:center}.empty-content__action[data-v-04d732c3]{margin-top:8px}.modal-wrapper .empty-content__action[data-v-04d732c3]{margin-top:20px;display:flex}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcEmptyContent/NcEmptyContent.vue"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCND,gCACC,YAAA,CACA,kBAAA,CACA,qBAAA,CACA,eAAA,CAEA,+CACC,cAAA,CACA,iBAAA,CAGD,sCACC,YAAA,CACA,kBAAA,CACA,sBAAA,CACA,UAAA,CACA,WAAA,CACA,kBAAA,CACA,UAAA,CACA,2BAAA,CACA,0BAAA,CACA,oBAAA,CAEA,0CACC,UAAA,CACA,WAAA,CAIF,uCACC,kBAAA,CACA,iBAAA,CAGD,wCACC,cAAA,CAEA,uDACC,eAAA,CACA,YAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n.empty-content {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-direction: column;\n\tmargin-top: 20vh;\n\n\t.modal-wrapper & {\n\t\tmargin-top: 5vh;\n\t\tmargin-bottom: 5vh;\n\t}\n\n\t&__icon {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 64px;\n\t\theight: 64px;\n\t\tmargin: 0 auto 15px;\n\t\topacity: .4;\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-position: center;\n\t\tbackground-size: 64px;\n\n\t\t:deep(svg) {\n\t\t\twidth: 64px;\n\t\t\theight: 64px;\n\t\t}\n\t}\n\n\t&__title {\n\t\tmargin-bottom: 10px;\n\t\ttext-align: center;\n\t}\n\n\t&__action {\n\t\tmargin-top: 8px;\n\n\t\t.modal-wrapper & {\n\t\t\tmargin-top: 20px;\n\t\t\tdisplay: flex;\n\t\t}\n\t}\n}\n"],sourceRoot:""}]);const s=i},5030:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-c4a9cada]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.loading-icon svg[data-v-c4a9cada]{animation:rotate var(--animation-duration, 0.8s) linear infinite}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcLoadingIcon/NcLoadingIcon.vue"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCND,mCACC,gEAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n.loading-icon svg{\n\tanimation: rotate var(--animation-duration, 0.8s) linear infinite;\n}\n"],sourceRoot:""}]);const s=i},4401:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.resize-observer{position:absolute;top:0;left:0;z-index:-1;width:100%;height:100%;border:none;background-color:rgba(0,0,0,0);pointer-events:none;display:block;overflow:hidden;opacity:0}.resize-observer object{display:block;position:absolute;top:0;left:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1}.v-popper--theme-dropdown.v-popper__popper{z-index:100000;top:0;left:0;display:block !important;filter:drop-shadow(0 1px 10px var(--color-box-shadow))}.v-popper--theme-dropdown.v-popper__popper .v-popper__inner{padding:0;color:var(--color-main-text);border-radius:var(--border-radius);overflow:hidden;background:var(--color-main-background)}.v-popper--theme-dropdown.v-popper__popper .v-popper__arrow-container{position:absolute;z-index:1;width:0;height:0;border-style:solid;border-color:rgba(0,0,0,0);border-width:10px}.v-popper--theme-dropdown.v-popper__popper[data-popper-placement^=top] .v-popper__arrow-container{bottom:-10px;border-bottom-width:0;border-top-color:var(--color-main-background)}.v-popper--theme-dropdown.v-popper__popper[data-popper-placement^=bottom] .v-popper__arrow-container{top:-10px;border-top-width:0;border-bottom-color:var(--color-main-background)}.v-popper--theme-dropdown.v-popper__popper[data-popper-placement^=right] .v-popper__arrow-container{left:-10px;border-left-width:0;border-right-color:var(--color-main-background)}.v-popper--theme-dropdown.v-popper__popper[data-popper-placement^=left] .v-popper__arrow-container{right:-10px;border-right-width:0;border-left-color:var(--color-main-background)}.v-popper--theme-dropdown.v-popper__popper[aria-hidden=true]{visibility:hidden;transition:opacity var(--animation-quick),visibility var(--animation-quick);opacity:0}.v-popper--theme-dropdown.v-popper__popper[aria-hidden=false]{visibility:visible;transition:opacity var(--animation-quick);opacity:1}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcPopover/NcPopover.vue"],names:[],mappings:"AAGA,sBACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCLD,iBACC,iBAAA,CACA,KAAA,CACA,MAAA,CACA,UAAA,CACA,UAAA,CACA,WAAA,CACA,WAAA,CACA,8BAAA,CACA,mBAAA,CACA,aAAA,CACA,eAAA,CACA,SAAA,CAGD,wBACC,aAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,WAAA,CACA,UAAA,CACA,eAAA,CACA,mBAAA,CACA,UAAA,CAMA,2CACC,cAAA,CACA,KAAA,CACA,MAAA,CACA,wBAAA,CAEA,sDAAA,CAEA,4DACC,SAAA,CACA,4BAAA,CACA,kCAAA,CACA,eAAA,CACA,uCAAA,CAGD,sEACC,iBAAA,CACA,SAAA,CACA,OAAA,CACA,QAAA,CACA,kBAAA,CACA,0BAAA,CACA,iBA1BW,CA6BZ,kGACC,YAAA,CACA,qBAAA,CACA,6CAAA,CAGD,qGACC,SAAA,CACA,kBAAA,CACA,gDAAA,CAGD,oGACC,UAAA,CACA,mBAAA,CACA,+CAAA,CAGD,mGACC,WAAA,CACA,oBAAA,CACA,8CAAA,CAGD,6DACC,iBAAA,CACA,2EAAA,CACA,SAAA,CAGD,8DACC,kBAAA,CACA,yCAAA,CACA,SAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n\n.resize-observer {\n\tposition:absolute;\n\ttop:0;\n\tleft:0;\n\tz-index:-1;\n\twidth:100%;\n\theight:100%;\n\tborder:none;\n\tbackground-color:transparent;\n\tpointer-events:none;\n\tdisplay:block;\n\toverflow:hidden;\n\topacity:0\n}\n\n.resize-observer object {\n\tdisplay:block;\n\tposition:absolute;\n\ttop:0;\n\tleft:0;\n\theight:100%;\n\twidth:100%;\n\toverflow:hidden;\n\tpointer-events:none;\n\tz-index:-1\n}\n\n$arrow-width: 10px;\n\n.v-popper--theme-dropdown {\n\t&.v-popper__popper {\n\t\tz-index: 100000;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tdisplay: block !important;\n\n\t\tfilter: drop-shadow(0 1px 10px var(--color-box-shadow));\n\n\t\t.v-popper__inner {\n\t\t\tpadding: 0;\n\t\t\tcolor: var(--color-main-text);\n\t\t\tborder-radius: var(--border-radius);\n\t\t\toverflow: hidden;\n\t\t\tbackground: var(--color-main-background);\n\t\t}\n\n\t\t.v-popper__arrow-container {\n\t\t\tposition: absolute;\n\t\t\tz-index: 1;\n\t\t\twidth: 0;\n\t\t\theight: 0;\n\t\t\tborder-style: solid;\n\t\t\tborder-color: transparent;\n\t\t\tborder-width: $arrow-width;\n\t\t}\n\n\t\t&[data-popper-placement^='top'] .v-popper__arrow-container {\n\t\t\tbottom: -$arrow-width;\n\t\t\tborder-bottom-width: 0;\n\t\t\tborder-top-color: var(--color-main-background);\n\t\t}\n\n\t\t&[data-popper-placement^='bottom'] .v-popper__arrow-container {\n\t\t\ttop: -$arrow-width;\n\t\t\tborder-top-width: 0;\n\t\t\tborder-bottom-color: var(--color-main-background);\n\t\t}\n\n\t\t&[data-popper-placement^='right'] .v-popper__arrow-container {\n\t\t\tleft: -$arrow-width;\n\t\t\tborder-left-width: 0;\n\t\t\tborder-right-color: var(--color-main-background);\n\t\t}\n\n\t\t&[data-popper-placement^='left'] .v-popper__arrow-container {\n\t\t\tright: -$arrow-width;\n\t\t\tborder-right-width: 0;\n\t\t\tborder-left-color: var(--color-main-background);\n\t\t}\n\n\t\t&[aria-hidden='true'] {\n\t\t\tvisibility: hidden;\n\t\t\ttransition: opacity var(--animation-quick), visibility var(--animation-quick);\n\t\t\topacity: 0;\n\t\t}\n\n\t\t&[aria-hidden='false'] {\n\t\t\tvisibility: visible;\n\t\t\ttransition: opacity var(--animation-quick);\n\t\t\topacity: 1;\n\t\t}\n\t}\n}\n\n"],sourceRoot:""}]);const s=i},3645:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,o,a){"string"==typeof e&&(e=[[null,e,void 0]]);var i={};if(r)for(var s=0;s0?" ".concat(c[5]):""," {").concat(c[1],"}")),c[5]=a),n&&(c[2]?(c[1]="@media ".concat(c[2]," {").concat(c[1],"}"),c[2]=n):c[2]=n),o&&(c[4]?(c[1]="@supports (".concat(c[4],") {").concat(c[1],"}"),c[4]=o):c[4]="".concat(o)),t.push(c))}},t}},7537:e=>{"use strict";e.exports=function(e){var t=e[1],n=e[3];if(!n)return t;if("function"==typeof btoa){var r=btoa(unescape(encodeURIComponent(JSON.stringify(n)))),o="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(r),a="/*# ".concat(o," */");return[t].concat([a]).join("\n")}return[t].join("\n")}},3379:e=>{"use strict";var t=[];function n(e){for(var n=-1,r=0;r{"use strict";var t={};e.exports=function(e,n){var r=function(e){if(void 0===t[e]){var n=document.querySelector(e);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}t[e]=n}return t[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(n)}},9216:e=>{"use strict";e.exports=function(e){var t=document.createElement("style");return e.setAttributes(t,e.attributes),e.insert(t,e.options),t}},3565:(e,t,n)=>{"use strict";e.exports=function(e){var t=n.nc;t&&e.setAttribute("nonce",t)}},7795:e=>{"use strict";e.exports=function(e){var t=e.insertStyleElement(e);return{update:function(n){!function(e,t,n){var r="";n.supports&&(r+="@supports (".concat(n.supports,") {")),n.media&&(r+="@media ".concat(n.media," {"));var o=void 0!==n.layer;o&&(r+="@layer".concat(n.layer.length>0?" ".concat(n.layer):""," {")),r+=n.css,o&&(r+="}"),n.media&&(r+="}"),n.supports&&(r+="}");var a=n.sourceMap;a&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a))))," */")),t.styleTagTransform(r,e,t.options)}(t,e,n)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(t)}}}},4589:e=>{"use strict";e.exports=function(e,t){if(t.styleSheet)t.styleSheet.cssText=e;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(e))}}},5727:()=>{},2112:()=>{},2102:()=>{},9258:()=>{},9280:()=>{},2405:()=>{},1900:(e,t,n)=>{"use strict";function r(e,t,n,r,o,a,i,s){var l,u="function"==typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),a&&(u._scopeId="data-v-"+a),i?(l=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(i)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var p=u.beforeCreate;u.beforeCreate=p?[].concat(p,l):[l]}return{exports:e,options:u}}n.d(t,{Z:()=>r})},7931:e=>{"use strict";e.exports=n(5918)},4055:e=>{"use strict";e.exports=n(9495)},9454:e=>{"use strict";e.exports=n(3045)},4505:e=>{"use strict";e.exports=n(4291)},2734:e=>{"use strict";e.exports=n(144)},3875:e=>{"use strict";e.exports=n(9429)},8618:e=>{"use strict";e.exports=n(2675)},1441:e=>{"use strict";e.exports=n(9115)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={id:n,exports:{}};return e[n](a,a.exports,r),a.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nc=void 0;var o={};return(()=>{"use strict";r.r(o),r.d(o,{default:()=>G});const e={name:"NcAppSidebarTabs",components:{NcVNodes:r(3329).default},provide:function(){var e=this;return{registerTab:this.registerTab,unregisterTab:this.unregisterTab,getActiveTab:function(){return e.activeTab}}},props:{active:{type:String,default:""}},emits:["update:active"],data:function(){return{tabs:[],activeTab:""}},computed:{hasMultipleTabs:function(){return this.tabs.length>1},currentTabIndex:function(){var e=this;return this.tabs.findIndex((function(t){return t.id===e.activeTab}))}},watch:{active:function(e){e!==this.activeTab&&this.updateActive()}},methods:{setActive:function(e){this.activeTab=e,this.$emit("update:active",this.activeTab)},focusPreviousTab:function(){this.currentTabIndex>0&&this.setActive(this.tabs[this.currentTabIndex-1].id),this.focusActiveTab()},focusNextTab:function(){this.currentTabIndex0?this.tabs[0].id:""},registerTab:function(e){this.tabs.push(e),this.tabs.sort((function(e,t){return e.order===t.order?OC.Util.naturalSortCompare(e.name,t.name):e.order-t.order})),this.updateActive()},unregisterTab:function(e){var t=this.tabs.findIndex((function(t){return t.id===e}));-1!==t&&this.tabs.splice(t,1),this.activeTab===e&&this.updateActive()}}};var t=r(3379),a=r.n(t),i=r(7795),s=r.n(i),l=r(569),u=r.n(l),c=r(3565),p=r.n(c),d=r(9216),f=r.n(d),m=r(4589),v=r.n(m),h=r(9290),g={};g.styleTagTransform=v(),g.setAttributes=p(),g.insert=u().bind(null,"head"),g.domAPI=s(),g.insertStyleElement=f(),a()(h.Z,g),h.Z&&h.Z.locals&&h.Z.locals;var b=r(1900);const y=(0,b.Z)(e,(function(){var e=this,t=e._self._c;return t("div",{staticClass:"app-sidebar-tabs"},[e.hasMultipleTabs?t("nav",{staticClass:"app-sidebar-tabs__nav",attrs:{role:"tablist"},on:{keydown:[function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"left",37,t.key,["Left","ArrowLeft"])||"button"in t&&0!==t.button||t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusPreviousTab.apply(null,arguments))},function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"right",39,t.key,["Right","ArrowRight"])||"button"in t&&2!==t.button||t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusNextTab.apply(null,arguments))},function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"tab",9,t.key,"Tab")||t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusActiveTabContent.apply(null,arguments))},function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"home",void 0,t.key,void 0)||t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusFirstTab.apply(null,arguments))},function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"end",void 0,t.key,void 0)||t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusLastTab.apply(null,arguments))},function(t){return t.type.indexOf("key")||33===t.keyCode?t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusFirstTab.apply(null,arguments)):null},function(t){return t.type.indexOf("key")||34===t.keyCode?t.ctrlKey||t.shiftKey||t.altKey||t.metaKey?null:(t.preventDefault(),e.focusLastTab.apply(null,arguments)):null}]}},[t("ul",e._l(e.tabs,(function(n){return t("li",{key:n.id,staticClass:"app-sidebar-tabs__tab"},[t("a",{class:{active:e.activeTab===n.id},attrs:{id:n.id,"aria-controls":"tab-".concat(n.id),"aria-selected":e.activeTab===n.id,"data-id":n.id,href:"#tab-".concat(n.id),tabindex:e.activeTab===n.id?0:-1,role:"tab"},on:{click:function(t){return t.preventDefault(),e.setActive(n.id)}}},[t("span",{staticClass:"app-sidebar-tabs__tab-icon"},[t("NcVNodes",{attrs:{vnodes:n.renderIcon()}},[t("span",{class:n.icon})])],1),e._v("\n\t\t\t\t\t"+e._s(n.name)+"\n\t\t\t\t")])])})),0)]):e._e(),e._v(" "),t("div",{staticClass:"app-sidebar-tabs__content",class:{"app-sidebar-tabs__content--multiple":e.hasMultipleTabs}},[e._t("default")],2)])}),[],!1,null,"204e1d5c",null).exports;var A=r(4891),w=r(5378),C=r(9104),x=r(3335),k=r(8167),_=r(5675),S=r(336),O=r(932),E=r(3875),T=r.n(E),P=r(8618),j=r.n(P);const N=n(4777);var D=r.n(N);const $=n(4603);var R=r.n($),F=r(4055);const I={name:"NcAppSidebar",components:{NcActions:A.default,NcAppSidebarTabs:y,ArrowRight:T(),NcButton:C.default,NcLoadingIcon:w.default,NcEmptyContent:x.default,Close:j(),Star:D(),StarOutline:R()},directives:{focus:k.default,linkify:_.default,ClickOutside:F.vOnClickOutside,Tooltip:S.default},props:{active:{type:String,default:""},title:{type:String,default:"",required:!0},titleEditable:{type:Boolean,default:!1},titlePlaceholder:{type:String,default:""},subtitle:{type:String,default:""},subtitleTooltip:{type:String,default:""},background:{type:String,default:""},starred:{type:Boolean,default:null},starLoading:{type:Boolean,default:!1},loading:{type:Boolean,default:!1},compact:{type:Boolean,default:!1},empty:{type:Boolean,default:!1},forceMenu:{type:Boolean,default:!1},linkifyTitle:{type:Boolean,default:!1},titleTooltip:{type:String,default:""}},emits:["close","closing","closed","opening","opened","figure-click","update:starred","update:titleEditable","update:title","update:active","submit-title","dismiss-editing"],data:function(){return{changeTitleTranslated:(0,O.t)("Change title"),closeTranslated:(0,O.t)("Close sidebar"),favoriteTranslated:(0,O.t)("Favorite"),isStarred:this.starred}},computed:{canStar:function(){return null!==this.isStarred},hasFigure:function(){return this.$slots.header||this.background},hasFigureClickListener:function(){return this.$listeners["figure-click"]}},watch:{starred:function(){this.isStarred=this.starred}},beforeDestroy:function(){this.$emit("closed")},methods:{onBeforeEnter:function(e){this.$emit("opening",e)},onAfterEnter:function(e){this.$emit("opened",e)},onBeforeLeave:function(e){this.$emit("closing",e)},onAfterLeave:function(e){this.$emit("closed",e)},closeSidebar:function(e){this.$emit("close",e)},onFigureClick:function(e){this.$emit("figure-click",e)},toggleStarred:function(){this.isStarred=!this.isStarred,this.$emit("update:starred",this.isStarred)},editTitle:function(){var e=this;this.$emit("update:titleEditable",!0),this.titleEditable&&this.$nextTick((function(){return e.$refs.titleInput.focus()}))},onTitleInput:function(e){this.$emit("update:title",e.target.value)},onSubmitTitle:function(e){this.$emit("update:titleEditable",!1),this.$emit("submit-title",e)},onDismissEditing:function(){this.$emit("update:titleEditable",!1),this.$emit("dismiss-editing")},onUpdateActive:function(e){this.$emit("update:active",e)}}};var L=r(6801),z={};z.styleTagTransform=v(),z.setAttributes=p(),z.insert=u().bind(null,"head"),z.domAPI=s(),z.insertStyleElement=f(),a()(L.Z,z),L.Z&&L.Z.locals&&L.Z.locals;var M=r(6180),B={};B.styleTagTransform=v(),B.setAttributes=p(),B.insert=u().bind(null,"head"),B.domAPI=s(),B.insertStyleElement=f(),a()(M.Z,B),M.Z&&M.Z.locals&&M.Z.locals;var U=r(2112),V=r.n(U),H=(0,b.Z)(I,(function(){var e=this,t=e._self._c;return t("transition",{attrs:{appear:"",name:"slide-right"},on:{"before-enter":e.onBeforeEnter,"after-enter":e.onAfterEnter,"before-leave":e.onBeforeLeave,"after-leave":e.onAfterLeave}},[t("aside",{staticClass:"app-sidebar",attrs:{id:"app-sidebar-vue"}},[t("header",{staticClass:"app-sidebar-header",class:{"app-sidebar-header--with-figure":e.hasFigure,"app-sidebar-header--compact":e.compact}},[t("div",{staticClass:"app-sidebar-header__info"},[e.hasFigure&&!e.empty?t("div",{staticClass:"app-sidebar-header__figure",class:{"app-sidebar-header__figure--with-action":e.hasFigureClickListener},style:{backgroundImage:"url(".concat(e.background,")")},attrs:{tabindex:"0"},on:{click:e.onFigureClick,keydown:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.onFigureClick.apply(null,arguments)}}},[e._t("header")],2):e._e(),e._v(" "),e.empty?e._e():t("div",{staticClass:"app-sidebar-header__desc",class:{"app-sidebar-header__desc--with-tertiary-action":e.canStar||e.$slots["tertiary-actions"],"app-sidebar-header__desc--editable":e.titleEditable&&!e.subtitle,"app-sidebar-header__desc--with-subtitle--editable":e.titleEditable&&e.subtitle,"app-sidebar-header__desc--without-actions":!e.$slots["secondary-actions"]}},[e.canStar||e.$slots["tertiary-actions"]?t("div",{staticClass:"app-sidebar-header__tertiary-actions"},[e._t("tertiary-actions",(function(){return[e.canStar?t("NcButton",{staticClass:"app-sidebar-header__star",attrs:{"aria-label":e.favoriteTranslated,type:"secondary"},on:{click:function(t){return t.preventDefault(),e.toggleStarred.apply(null,arguments)}},scopedSlots:e._u([{key:"icon",fn:function(){return[e.starLoading?t("NcLoadingIcon"):e.isStarred?t("Star",{attrs:{size:20}}):t("StarOutline",{attrs:{size:20}})]},proxy:!0}],null,!1,2575459756)}):e._e()]}))],2):e._e(),e._v(" "),t("div",{staticClass:"app-sidebar-header__title-container"},[t("div",{staticClass:"app-sidebar-header__maintitle-container"},[t("h2",{directives:[{name:"show",rawName:"v-show",value:!e.titleEditable,expression:"!titleEditable"},{name:"linkify",rawName:"v-linkify",value:{text:e.title,linkify:e.linkifyTitle},expression:"{text: title, linkify: linkifyTitle}"}],staticClass:"app-sidebar-header__maintitle",attrs:{"aria-label":e.titleTooltip,title:e.titleTooltip,tabindex:e.titleEditable?0:void 0},on:{click:function(t){return t.target!==t.currentTarget?null:e.editTitle.apply(null,arguments)}}},[e._v("\n\t\t\t\t\t\t\t\t"+e._s(e.title)+"\n\t\t\t\t\t\t\t")]),e._v(" "),e.titleEditable?[t("form",{directives:[{name:"click-outside",rawName:"v-click-outside",value:function(){return e.onSubmitTitle()},expression:"() => onSubmitTitle()"}],staticClass:"app-sidebar-header__maintitle-form",on:{submit:function(t){return t.preventDefault(),e.onSubmitTitle.apply(null,arguments)}}},[t("input",{directives:[{name:"focus",rawName:"v-focus"}],ref:"titleInput",staticClass:"app-sidebar-header__maintitle-input",attrs:{type:"text",placeholder:e.titlePlaceholder},domProps:{value:e.title},on:{keydown:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"esc",27,t.key,["Esc","Escape"])?null:e.onDismissEditing.apply(null,arguments)},input:e.onTitleInput}}),e._v(" "),t("NcButton",{attrs:{type:"tertiary-no-background","aria-label":e.changeTitleTranslated,"native-type":"submit"},scopedSlots:e._u([{key:"icon",fn:function(){return[t("ArrowRight",{attrs:{size:20}})]},proxy:!0}],null,!1,1252225425)})],1)]:e._e(),e._v(" "),e.$slots["secondary-actions"]?t("NcActions",{staticClass:"app-sidebar-header__menu",attrs:{"force-menu":e.forceMenu}},[e._t("secondary-actions")],2):e._e()],2),e._v(" "),""!==e.subtitle.trim()?t("p",{staticClass:"app-sidebar-header__subtitle",attrs:{"aria-label":e.subtitleTooltip,title:e.subtitleTooltip}},[e._v("\n\t\t\t\t\t\t\t"+e._s(e.subtitle)+"\n\t\t\t\t\t\t")]):e._e()])])]),e._v(" "),t("NcButton",{staticClass:"app-sidebar__close",attrs:{title:e.closeTranslated,"aria-label":e.closeTranslated,type:"tertiary"},on:{click:function(t){return t.preventDefault(),e.closeSidebar.apply(null,arguments)}},scopedSlots:e._u([{key:"icon",fn:function(){return[t("Close",{attrs:{size:20}})]},proxy:!0}])}),e._v(" "),e.$slots.description&&!e.empty?t("div",{staticClass:"app-sidebar-header__description"},[e._t("description")],2):e._e()],1),e._v(" "),t("NcAppSidebarTabs",{directives:[{name:"show",rawName:"v-show",value:!e.loading,expression:"!loading"}],ref:"tabs",attrs:{active:e.active},on:{"update:active":e.onUpdateActive}},[e._t("default")],2),e._v(" "),e.loading?t("NcEmptyContent",{scopedSlots:e._u([{key:"icon",fn:function(){return[t("NcLoadingIcon",{attrs:{size:64}})]},proxy:!0}],null,!1,826850984)}):e._e()],1)])}),[],!1,null,"62b02a03",null);"function"==typeof V()&&V()(H);const G=H.exports})(),o})()))},2574:e=>{!function(t,n){e.exports=n()}(self,(()=>(()=>{"use strict";var e={8222:(e,t,n)=>{n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,".material-design-icon[data-v-0c059703]{display:flex;align-self:center;justify-self:center;align-items:center;justify-content:center}.app-sidebar__tab[data-v-0c059703]{display:none;padding:10px;min-height:100%;max-height:100%;height:100%;overflow:auto}.app-sidebar__tab[data-v-0c059703]:focus{border-color:var(--color-primary-element);box-shadow:0 0 .2em var(--color-primary-element);outline:0}.app-sidebar__tab--active[data-v-0c059703]{display:block}","",{version:3,sources:["webpack://./src/assets/material-icons.css","webpack://./src/components/NcAppSidebarTab/NcAppSidebarTab.vue"],names:[],mappings:"AAGA,uCACC,YAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,sBAAA,CCND,mCACC,YAAA,CACA,YAAA,CACA,eAAA,CACA,eAAA,CACA,WAAA,CACA,aAAA,CAEA,yCACC,yCAAA,CACA,gDAAA,CACA,SAAA,CAGD,2CACC,aAAA",sourcesContent:["/*\n* Ensure proper alignment of the vue material icons\n*/\n.material-design-icon {\n\tdisplay: flex;\n\talign-self: center;\n\tjustify-self: center;\n\talign-items: center;\n\tjustify-content: center;\n}\n","@use 'sass:math'; $scope_version:\"caee4c9\"; @import 'variables'; @import 'material-icons';\n\n.app-sidebar__tab {\n\tdisplay: none;\n\tpadding: 10px;\n\tmin-height: 100%; // fill available height\n\tmax-height: 100%; // scroll inside\n\theight: 100%;\n\toverflow: auto;\n\n\t&:focus {\n\t\tborder-color: var(--color-primary-element);\n\t\tbox-shadow: 0 0 0.2em var(--color-primary-element);\n\t\toutline: 0;\n\t}\n\n\t&--active {\n\t\tdisplay: block;\n\t}\n}\n"],sourceRoot:""}]);const s=i},3645:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,o,a){"string"==typeof e&&(e=[[null,e,void 0]]);var i={};if(r)for(var s=0;s0?" ".concat(c[5]):""," {").concat(c[1],"}")),c[5]=a),n&&(c[2]?(c[1]="@media ".concat(c[2]," {").concat(c[1],"}"),c[2]=n):c[2]=n),o&&(c[4]?(c[1]="@supports (".concat(c[4],") {").concat(c[1],"}"),c[4]=o):c[4]="".concat(o)),t.push(c))}},t}},7537:e=>{e.exports=function(e){var t=e[1],n=e[3];if(!n)return t;if("function"==typeof btoa){var r=btoa(unescape(encodeURIComponent(JSON.stringify(n)))),o="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(r),a="/*# ".concat(o," */");return[t].concat([a]).join("\n")}return[t].join("\n")}},3379:e=>{var t=[];function n(e){for(var n=-1,r=0;r{var t={};e.exports=function(e,n){var r=function(e){if(void 0===t[e]){var n=document.querySelector(e);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}t[e]=n}return t[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(n)}},9216:e=>{e.exports=function(e){var t=document.createElement("style");return e.setAttributes(t,e.attributes),e.insert(t,e.options),t}},3565:(e,t,n)=>{e.exports=function(e){var t=n.nc;t&&e.setAttribute("nonce",t)}},7795:e=>{e.exports=function(e){var t=e.insertStyleElement(e);return{update:function(n){!function(e,t,n){var r="";n.supports&&(r+="@supports (".concat(n.supports,") {")),n.media&&(r+="@media ".concat(n.media," {"));var o=void 0!==n.layer;o&&(r+="@layer".concat(n.layer.length>0?" ".concat(n.layer):""," {")),r+=n.css,o&&(r+="}"),n.media&&(r+="}"),n.supports&&(r+="}");var a=n.sourceMap;a&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a))))," */")),t.styleTagTransform(r,e,t.options)}(t,e,n)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(t)}}}},4589:e=>{e.exports=function(e,t){if(t.styleSheet)t.styleSheet.cssText=e;else{for(;t.firstChild;)t.removeChild(t.firstChild);t.appendChild(document.createTextNode(e))}}},1900:(e,t,n)=>{function r(e,t,n,r,o,a,i,s){var l,u="function"==typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),a&&(u._scopeId="data-v-"+a),i?(l=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(i)},u._ssrRegister=l):o&&(l=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(e,t){return l.call(t),c(e,t)}}else{var p=u.beforeCreate;u.beforeCreate=p?[].concat(p,l):[l]}return{exports:e,options:u}}n.d(t,{Z:()=>r})}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var a=t[r]={id:r,exports:{}};return e[r](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nc=void 0;var r={};return(()=>{n.r(r),n.d(r,{default:()=>g});const e={name:"NcAppSidebarTab",inject:["registerTab","unregisterTab","getActiveTab"],props:{id:{type:String,required:!0},name:{type:String,required:!0},icon:{type:String,default:""},order:{type:Number,default:0}},emits:["bottom-reached","scroll"],expose:["id","name","icon","order","renderIcon"],computed:{isActive:function(){return this.getActiveTab()===this.id}},created:function(){this.registerTab(this)},beforeDestroy:function(){this.unregisterTab(this.id)},methods:{onScroll:function(e){this.$el.scrollHeight-this.$el.scrollTop===this.$el.clientHeight&&this.$emit("bottom-reached",e),this.$emit("scroll",e)},renderIcon:function(){var e,t;return null===(e=(t=this.$scopedSlots).icon)||void 0===e?void 0:e.call(t)}}};var t=n(3379),o=n.n(t),a=n(7795),i=n.n(a),s=n(569),l=n.n(s),u=n(3565),c=n.n(u),p=n(9216),d=n.n(p),f=n(4589),m=n.n(f),v=n(8222),h={};h.styleTagTransform=m(),h.setAttributes=c(),h.insert=l().bind(null,"head"),h.domAPI=i(),h.insertStyleElement=d(),o()(v.Z,h),v.Z&&v.Z.locals&&v.Z.locals;const g=(0,n(1900).Z)(e,(function(){var e=this,t=e._self._c;return t("section",{staticClass:"app-sidebar__tab",class:{"app-sidebar__tab--active":e.isActive},attrs:{id:"tab-".concat(e.id),"aria-hidden":!e.isActive,"aria-labelledby":e.id,tabindex:"0",role:"tabpanel"},on:{scroll:e.onScroll}},[t("h3",{staticClass:"hidden-visually"},[e._v("\n\t\t"+e._s(e.name)+"\n\t")]),e._v(" "),e._t("default")],2)}),[],!1,null,"0c059703",null).exports})(),r})()))},5918:(e,t,n)=>{"use strict";var r=n(7699);n(9753),n(7856),n(5573);class o{constructor(){this.translations={},this.debug=!1}setLanguage(e){return this.locale=e,this}detectLocale(){return this.setLanguage((document.documentElement.lang||"en").replace("-","_"))}addTranslation(e,t){return this.translations[e]=t,this}enableDebugMode(){return this.debug=!0,this}build(){return new a(this.locale||"en",this.translations,this.debug)}}class a{constructor(e,t,n){this.gt=new r({debug:n,sourceLocale:"en"});for(const e in t)this.gt.addTranslations(e,"messages",t[e]);this.gt.setLocale(e)}subtitudePlaceholders(e,t){return e.replace(/{([^{}]*)}/g,((e,n)=>{const r=t[n];return"string"==typeof r||"number"==typeof r?r.toString():e}))}gettext(e,t={}){return this.subtitudePlaceholders(this.gt.gettext(e),t)}ngettext(e,t,n,r={}){return this.subtitudePlaceholders(this.gt.ngettext(e,t,n).replace(/%n/g,n.toString()),r)}}t.getGettextBuilder=function(){return new o}},9669:(e,t,n)=>{e.exports=n(1609)},5448:(e,t,n)=>{"use strict";var r=n(4867),o=n(6026),a=n(4372),i=n(5327),s=n(4097),l=n(4109),u=n(7985),c=n(7874),p=n(2648),d=n(644),f=n(205);e.exports=function(e){return new Promise((function(t,n){var m,v=e.data,h=e.headers,g=e.responseType;function b(){e.cancelToken&&e.cancelToken.unsubscribe(m),e.signal&&e.signal.removeEventListener("abort",m)}r.isFormData(v)&&r.isStandardBrowserEnv()&&delete h["Content-Type"];var y=new XMLHttpRequest;if(e.auth){var A=e.auth.username||"",w=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";h.Authorization="Basic "+btoa(A+":"+w)}var C=s(e.baseURL,e.url);function x(){if(y){var r="getAllResponseHeaders"in y?l(y.getAllResponseHeaders()):null,a={data:g&&"text"!==g&&"json"!==g?y.response:y.responseText,status:y.status,statusText:y.statusText,headers:r,config:e,request:y};o((function(e){t(e),b()}),(function(e){n(e),b()}),a),y=null}}if(y.open(e.method.toUpperCase(),i(C,e.params,e.paramsSerializer),!0),y.timeout=e.timeout,"onloadend"in y?y.onloadend=x:y.onreadystatechange=function(){y&&4===y.readyState&&(0!==y.status||y.responseURL&&0===y.responseURL.indexOf("file:"))&&setTimeout(x)},y.onabort=function(){y&&(n(new p("Request aborted",p.ECONNABORTED,e,y)),y=null)},y.onerror=function(){n(new p("Network Error",p.ERR_NETWORK,e,y,y)),y=null},y.ontimeout=function(){var t=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded",r=e.transitional||c;e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(new p(t,r.clarifyTimeoutError?p.ETIMEDOUT:p.ECONNABORTED,e,y)),y=null},r.isStandardBrowserEnv()){var k=(e.withCredentials||u(C))&&e.xsrfCookieName?a.read(e.xsrfCookieName):void 0;k&&(h[e.xsrfHeaderName]=k)}"setRequestHeader"in y&&r.forEach(h,(function(e,t){void 0===v&&"content-type"===t.toLowerCase()?delete h[t]:y.setRequestHeader(t,e)})),r.isUndefined(e.withCredentials)||(y.withCredentials=!!e.withCredentials),g&&"json"!==g&&(y.responseType=e.responseType),"function"==typeof e.onDownloadProgress&&y.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&y.upload&&y.upload.addEventListener("progress",e.onUploadProgress),(e.cancelToken||e.signal)&&(m=function(e){y&&(n(!e||e&&e.type?new d:e),y.abort(),y=null)},e.cancelToken&&e.cancelToken.subscribe(m),e.signal&&(e.signal.aborted?m():e.signal.addEventListener("abort",m))),v||(v=null);var _=f(C);_&&-1===["http","https","file"].indexOf(_)?n(new p("Unsupported protocol "+_+":",p.ERR_BAD_REQUEST,e)):y.send(v)}))}},1609:(e,t,n)=>{"use strict";var r=n(4867),o=n(1849),a=n(321),i=n(7185);var s=function e(t){var n=new a(t),s=o(a.prototype.request,n);return r.extend(s,a.prototype,n),r.extend(s,n),s.create=function(n){return e(i(t,n))},s}(n(5546));s.Axios=a,s.CanceledError=n(644),s.CancelToken=n(4972),s.isCancel=n(6502),s.VERSION=n(7288).version,s.toFormData=n(7675),s.AxiosError=n(2648),s.Cancel=s.CanceledError,s.all=function(e){return Promise.all(e)},s.spread=n(8713),s.isAxiosError=n(6268),e.exports=s,e.exports.default=s},4972:(e,t,n)=>{"use strict";var r=n(644);function o(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise((function(e){t=e}));var n=this;this.promise.then((function(e){if(n._listeners){var t,r=n._listeners.length;for(t=0;t{"use strict";var r=n(2648);function o(e){r.call(this,null==e?"canceled":e,r.ERR_CANCELED),this.name="CanceledError"}n(4867).inherits(o,r,{__CANCEL__:!0}),e.exports=o},6502:e=>{"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},321:(e,t,n)=>{"use strict";var r=n(4867),o=n(5327),a=n(782),i=n(3572),s=n(7185),l=n(4097),u=n(4875),c=u.validators;function p(e){this.defaults=e,this.interceptors={request:new a,response:new a}}p.prototype.request=function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{},(t=s(this.defaults,t)).method?t.method=t.method.toLowerCase():this.defaults.method?t.method=this.defaults.method.toLowerCase():t.method="get";var n=t.transitional;void 0!==n&&u.assertOptions(n,{silentJSONParsing:c.transitional(c.boolean),forcedJSONParsing:c.transitional(c.boolean),clarifyTimeoutError:c.transitional(c.boolean)},!1);var r=[],o=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(o=o&&e.synchronous,r.unshift(e.fulfilled,e.rejected))}));var a,l=[];if(this.interceptors.response.forEach((function(e){l.push(e.fulfilled,e.rejected)})),!o){var p=[i,void 0];for(Array.prototype.unshift.apply(p,r),p=p.concat(l),a=Promise.resolve(t);p.length;)a=a.then(p.shift(),p.shift());return a}for(var d=t;r.length;){var f=r.shift(),m=r.shift();try{d=f(d)}catch(e){m(e);break}}try{a=i(d)}catch(e){return Promise.reject(e)}for(;l.length;)a=a.then(l.shift(),l.shift());return a},p.prototype.getUri=function(e){e=s(this.defaults,e);var t=l(e.baseURL,e.url);return o(t,e.params,e.paramsSerializer)},r.forEach(["delete","get","head","options"],(function(e){p.prototype[e]=function(t,n){return this.request(s(n||{},{method:e,url:t,data:(n||{}).data}))}})),r.forEach(["post","put","patch"],(function(e){function t(t){return function(n,r,o){return this.request(s(o||{},{method:e,headers:t?{"Content-Type":"multipart/form-data"}:{},url:n,data:r}))}}p.prototype[e]=t(),p.prototype[e+"Form"]=t(!0)})),e.exports=p},2648:(e,t,n)=>{"use strict";var r=n(4867);function o(e,t,n,r,o){Error.call(this),this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}r.inherits(o,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var a=o.prototype,i={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED"].forEach((function(e){i[e]={value:e}})),Object.defineProperties(o,i),Object.defineProperty(a,"isAxiosError",{value:!0}),o.from=function(e,t,n,i,s,l){var u=Object.create(a);return r.toFlatObject(e,u,(function(e){return e!==Error.prototype})),o.call(u,e.message,t,n,i,s),u.name=e.name,l&&Object.assign(u,l),u},e.exports=o},782:(e,t,n)=>{"use strict";var r=n(4867);function o(){this.handlers=[]}o.prototype.use=function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1},o.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},o.prototype.forEach=function(e){r.forEach(this.handlers,(function(t){null!==t&&e(t)}))},e.exports=o},4097:(e,t,n)=>{"use strict";var r=n(1793),o=n(7303);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},3572:(e,t,n)=>{"use strict";var r=n(4867),o=n(8527),a=n(6502),i=n(5546),s=n(644);function l(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new s}e.exports=function(e){return l(e),e.headers=e.headers||{},e.data=o.call(e,e.data,e.headers,e.transformRequest),e.headers=r.merge(e.headers.common||{},e.headers[e.method]||{},e.headers),r.forEach(["delete","get","head","post","put","patch","common"],(function(t){delete e.headers[t]})),(e.adapter||i.adapter)(e).then((function(t){return l(e),t.data=o.call(e,t.data,t.headers,e.transformResponse),t}),(function(t){return a(t)||(l(e),t&&t.response&&(t.response.data=o.call(e,t.response.data,t.response.headers,e.transformResponse))),Promise.reject(t)}))}},7185:(e,t,n)=>{"use strict";var r=n(4867);e.exports=function(e,t){t=t||{};var n={};function o(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function a(n){return r.isUndefined(t[n])?r.isUndefined(e[n])?void 0:o(void 0,e[n]):o(e[n],t[n])}function i(e){if(!r.isUndefined(t[e]))return o(void 0,t[e])}function s(n){return r.isUndefined(t[n])?r.isUndefined(e[n])?void 0:o(void 0,e[n]):o(void 0,t[n])}function l(n){return n in t?o(e[n],t[n]):n in e?o(void 0,e[n]):void 0}var u={url:i,method:i,data:i,baseURL:s,transformRequest:s,transformResponse:s,paramsSerializer:s,timeout:s,timeoutMessage:s,withCredentials:s,adapter:s,responseType:s,xsrfCookieName:s,xsrfHeaderName:s,onUploadProgress:s,onDownloadProgress:s,decompress:s,maxContentLength:s,maxBodyLength:s,beforeRedirect:s,transport:s,httpAgent:s,httpsAgent:s,cancelToken:s,socketPath:s,responseEncoding:s,validateStatus:l};return r.forEach(Object.keys(e).concat(Object.keys(t)),(function(e){var t=u[e]||a,o=t(e);r.isUndefined(o)&&t!==l||(n[e]=o)})),n}},6026:(e,t,n)=>{"use strict";var r=n(2648);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(new r("Request failed with status code "+n.status,[r.ERR_BAD_REQUEST,r.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}},8527:(e,t,n)=>{"use strict";var r=n(4867),o=n(5546);e.exports=function(e,t,n){var a=this||o;return r.forEach(n,(function(n){e=n.call(a,e,t)})),e}},5546:(e,t,n)=>{"use strict";var r=n(4155),o=n(4867),a=n(6016),i=n(2648),s=n(7874),l=n(7675),u={"Content-Type":"application/x-www-form-urlencoded"};function c(e,t){!o.isUndefined(e)&&o.isUndefined(e["Content-Type"])&&(e["Content-Type"]=t)}var p,d={transitional:s,adapter:(("undefined"!=typeof XMLHttpRequest||void 0!==r&&"[object process]"===Object.prototype.toString.call(r))&&(p=n(5448)),p),transformRequest:[function(e,t){if(a(t,"Accept"),a(t,"Content-Type"),o.isFormData(e)||o.isArrayBuffer(e)||o.isBuffer(e)||o.isStream(e)||o.isFile(e)||o.isBlob(e))return e;if(o.isArrayBufferView(e))return e.buffer;if(o.isURLSearchParams(e))return c(t,"application/x-www-form-urlencoded;charset=utf-8"),e.toString();var n,r=o.isObject(e),i=t&&t["Content-Type"];if((n=o.isFileList(e))||r&&"multipart/form-data"===i){var s=this.env&&this.env.FormData;return l(n?{"files[]":e}:e,s&&new s)}return r||"application/json"===i?(c(t,"application/json"),function(e,t,n){if(o.isString(e))try{return(t||JSON.parse)(e),o.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||d.transitional,n=t&&t.silentJSONParsing,r=t&&t.forcedJSONParsing,a=!n&&"json"===this.responseType;if(a||r&&o.isString(e)&&e.length)try{return JSON.parse(e)}catch(e){if(a){if("SyntaxError"===e.name)throw i.from(e,i.ERR_BAD_RESPONSE,this,null,this.response);throw e}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:n(1623)},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};o.forEach(["delete","get","head"],(function(e){d.headers[e]={}})),o.forEach(["post","put","patch"],(function(e){d.headers[e]=o.merge(u)})),e.exports=d},7874:e=>{"use strict";e.exports={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1}},7288:e=>{e.exports={version:"0.27.2"}},1849:e=>{"use strict";e.exports=function(e,t){return function(){for(var n=new Array(arguments.length),r=0;r{"use strict";var r=n(4867);function o(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}e.exports=function(e,t,n){if(!t)return e;var a;if(n)a=n(t);else if(r.isURLSearchParams(t))a=t.toString();else{var i=[];r.forEach(t,(function(e,t){null!=e&&(r.isArray(e)?t+="[]":e=[e],r.forEach(e,(function(e){r.isDate(e)?e=e.toISOString():r.isObject(e)&&(e=JSON.stringify(e)),i.push(o(t)+"="+o(e))})))})),a=i.join("&")}if(a){var s=e.indexOf("#");-1!==s&&(e=e.slice(0,s)),e+=(-1===e.indexOf("?")?"?":"&")+a}return e}},7303:e=>{"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},4372:(e,t,n)=>{"use strict";var r=n(4867);e.exports=r.isStandardBrowserEnv()?{write:function(e,t,n,o,a,i){var s=[];s.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&s.push("expires="+new Date(n).toGMTString()),r.isString(o)&&s.push("path="+o),r.isString(a)&&s.push("domain="+a),!0===i&&s.push("secure"),document.cookie=s.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}:{write:function(){},read:function(){return null},remove:function(){}}},1793:e=>{"use strict";e.exports=function(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}},6268:(e,t,n)=>{"use strict";var r=n(4867);e.exports=function(e){return r.isObject(e)&&!0===e.isAxiosError}},7985:(e,t,n)=>{"use strict";var r=n(4867);e.exports=r.isStandardBrowserEnv()?function(){var e,t=/(msie|trident)/i.test(navigator.userAgent),n=document.createElement("a");function o(e){var r=e;return t&&(n.setAttribute("href",r),r=n.href),n.setAttribute("href",r),{href:n.href,protocol:n.protocol?n.protocol.replace(/:$/,""):"",host:n.host,search:n.search?n.search.replace(/^\?/,""):"",hash:n.hash?n.hash.replace(/^#/,""):"",hostname:n.hostname,port:n.port,pathname:"/"===n.pathname.charAt(0)?n.pathname:"/"+n.pathname}}return e=o(window.location.href),function(t){var n=r.isString(t)?o(t):t;return n.protocol===e.protocol&&n.host===e.host}}():function(){return!0}},6016:(e,t,n)=>{"use strict";var r=n(4867);e.exports=function(e,t){r.forEach(e,(function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])}))}},1623:e=>{e.exports=null},4109:(e,t,n)=>{"use strict";var r=n(4867),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,a,i={};return e?(r.forEach(e.split("\n"),(function(e){if(a=e.indexOf(":"),t=r.trim(e.substr(0,a)).toLowerCase(),n=r.trim(e.substr(a+1)),t){if(i[t]&&o.indexOf(t)>=0)return;i[t]="set-cookie"===t?(i[t]?i[t]:[]).concat([n]):i[t]?i[t]+", "+n:n}})),i):i}},205:e=>{"use strict";e.exports=function(e){var t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}},8713:e=>{"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},7675:(e,t,n)=>{"use strict";var r=n(4867);e.exports=function(e,t){t=t||new FormData;var n=[];function o(e){return null===e?"":r.isDate(e)?e.toISOString():r.isArrayBuffer(e)||r.isTypedArray(e)?"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}return function e(a,i){if(r.isPlainObject(a)||r.isArray(a)){if(-1!==n.indexOf(a))throw Error("Circular reference detected in "+i);n.push(a),r.forEach(a,(function(n,a){if(!r.isUndefined(n)){var s,l=i?i+"."+a:a;if(n&&!i&&"object"==typeof n)if(r.endsWith(a,"{}"))n=JSON.stringify(n);else if(r.endsWith(a,"[]")&&(s=r.toArray(n)))return void s.forEach((function(e){!r.isUndefined(e)&&t.append(l,o(e))}));e(n,l)}})),n.pop()}else t.append(i,o(a))}(e),t}},4875:(e,t,n)=>{"use strict";var r=n(7288).version,o=n(2648),a={};["object","boolean","number","function","string","symbol"].forEach((function(e,t){a[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));var i={};a.transitional=function(e,t,n){function a(e,t){return"[Axios v"+r+"] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,r,s){if(!1===e)throw new o(a(r," has been removed"+(t?" in "+t:"")),o.ERR_DEPRECATED);return t&&!i[r]&&(i[r]=!0,console.warn(a(r," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,r,s)}},e.exports={assertOptions:function(e,t,n){if("object"!=typeof e)throw new o("options must be an object",o.ERR_BAD_OPTION_VALUE);for(var r=Object.keys(e),a=r.length;a-- >0;){var i=r[a],s=t[i];if(s){var l=e[i],u=void 0===l||s(l,i,e);if(!0!==u)throw new o("option "+i+" must be "+u,o.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new o("Unknown option "+i,o.ERR_BAD_OPTION)}},validators:a}},4867:(e,t,n)=>{"use strict";var r,o=n(1849),a=Object.prototype.toString,i=(r=Object.create(null),function(e){var t=a.call(e);return r[t]||(r[t]=t.slice(8,-1).toLowerCase())});function s(e){return e=e.toLowerCase(),function(t){return i(t)===e}}function l(e){return Array.isArray(e)}function u(e){return void 0===e}var c=s("ArrayBuffer");function p(e){return null!==e&&"object"==typeof e}function d(e){if("object"!==i(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}var f=s("Date"),m=s("File"),v=s("Blob"),h=s("FileList");function g(e){return"[object Function]"===a.call(e)}var b=s("URLSearchParams");function y(e,t){if(null!=e)if("object"!=typeof e&&(e=[e]),l(e))for(var n=0,r=e.length;n0;)i[a=r[o]]||(t[a]=e[a],i[a]=!0);e=Object.getPrototypeOf(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:i,kindOfTest:s,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;var t=e.length;if(u(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},isTypedArray:w,isFileList:h}},9662:(e,t,n)=>{var r=n(614),o=n(6330),a=TypeError;e.exports=function(e){if(r(e))return e;throw a(o(e)+" is not a function")}},1530:(e,t,n)=>{"use strict";var r=n(8710).charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},9670:(e,t,n)=>{var r=n(111),o=String,a=TypeError;e.exports=function(e){if(r(e))return e;throw a(o(e)+" is not an object")}},1318:(e,t,n)=>{var r=n(5656),o=n(1400),a=n(6244),i=function(e){return function(t,n,i){var s,l=r(t),u=a(l),c=o(i,u);if(e&&n!=n){for(;u>c;)if((s=l[c++])!=s)return!0}else for(;u>c;c++)if((e||c in l)&&l[c]===n)return e||c||0;return!e&&-1}};e.exports={includes:i(!0),indexOf:i(!1)}},9341:(e,t,n)=>{"use strict";var r=n(7293);e.exports=function(e,t){var n=[][e];return!!n&&r((function(){n.call(null,t||function(){return 1},1)}))}},4326:(e,t,n)=>{var r=n(1702),o=r({}.toString),a=r("".slice);e.exports=function(e){return a(o(e),8,-1)}},648:(e,t,n)=>{var r=n(1694),o=n(614),a=n(4326),i=n(5112)("toStringTag"),s=Object,l="Arguments"==a(function(){return arguments}());e.exports=r?a:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=s(e),i))?n:l?a(t):"Object"==(r=a(t))&&o(t.callee)?"Arguments":r}},9920:(e,t,n)=>{var r=n(2597),o=n(3887),a=n(1236),i=n(3070);e.exports=function(e,t,n){for(var s=o(t),l=i.f,u=a.f,c=0;c{var r=n(9781),o=n(3070),a=n(9114);e.exports=r?function(e,t,n){return o.f(e,t,a(1,n))}:function(e,t,n){return e[t]=n,e}},9114:e=>{e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},8052:(e,t,n)=>{var r=n(614),o=n(3070),a=n(6339),i=n(3072);e.exports=function(e,t,n,s){s||(s={});var l=s.enumerable,u=void 0!==s.name?s.name:t;if(r(n)&&a(n,u,s),s.global)l?e[t]=n:i(t,n);else{try{s.unsafe?e[t]&&(l=!0):delete e[t]}catch(e){}l?e[t]=n:o.f(e,t,{value:n,enumerable:!1,configurable:!s.nonConfigurable,writable:!s.nonWritable})}return e}},3072:(e,t,n)=>{var r=n(7854),o=Object.defineProperty;e.exports=function(e,t){try{o(r,e,{value:t,configurable:!0,writable:!0})}catch(n){r[e]=t}return t}},9781:(e,t,n)=>{var r=n(7293);e.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},4154:e=>{var t="object"==typeof document&&document.all,n=void 0===t&&void 0!==t;e.exports={all:t,IS_HTMLDDA:n}},317:(e,t,n)=>{var r=n(7854),o=n(111),a=r.document,i=o(a)&&o(a.createElement);e.exports=function(e){return i?a.createElement(e):{}}},8113:e=>{e.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},7392:(e,t,n)=>{var r,o,a=n(7854),i=n(8113),s=a.process,l=a.Deno,u=s&&s.versions||l&&l.version,c=u&&u.v8;c&&(o=(r=c.split("."))[0]>0&&r[0]<4?1:+(r[0]+r[1])),!o&&i&&(!(r=i.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=i.match(/Chrome\/(\d+)/))&&(o=+r[1]),e.exports=o},748:e=>{e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:(e,t,n)=>{var r=n(7854),o=n(1236).f,a=n(8880),i=n(8052),s=n(3072),l=n(9920),u=n(4705);e.exports=function(e,t){var n,c,p,d,f,m=e.target,v=e.global,h=e.stat;if(n=v?r:h?r[m]||s(m,{}):(r[m]||{}).prototype)for(c in t){if(d=t[c],p=e.dontCallGetSet?(f=o(n,c))&&f.value:n[c],!u(v?c:m+(h?".":"#")+c,e.forced)&&void 0!==p){if(typeof d==typeof p)continue;l(d,p)}(e.sham||p&&p.sham)&&a(d,"sham",!0),i(n,c,d,e)}}},7293:e=>{e.exports=function(e){try{return!!e()}catch(e){return!0}}},7007:(e,t,n)=>{"use strict";n(4916);var r=n(1470),o=n(8052),a=n(2261),i=n(7293),s=n(5112),l=n(8880),u=s("species"),c=RegExp.prototype;e.exports=function(e,t,n,p){var d=s(e),f=!i((function(){var t={};return t[d]=function(){return 7},7!=""[e](t)})),m=f&&!i((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[u]=function(){return n},n.flags="",n[d]=/./[d]),n.exec=function(){return t=!0,null},n[d](""),!t}));if(!f||!m||n){var v=r(/./[d]),h=t(d,""[e],(function(e,t,n,o,i){var s=r(e),l=t.exec;return l===a||l===c.exec?f&&!i?{done:!0,value:v(t,n,o)}:{done:!0,value:s(n,t,o)}:{done:!1}}));o(String.prototype,e,h[0]),o(c,d,h[1])}p&&l(c[d],"sham",!0)}},2104:(e,t,n)=>{var r=n(4374),o=Function.prototype,a=o.apply,i=o.call;e.exports="object"==typeof Reflect&&Reflect.apply||(r?i.bind(a):function(){return i.apply(a,arguments)})},4374:(e,t,n)=>{var r=n(7293);e.exports=!r((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")}))},6916:(e,t,n)=>{var r=n(4374),o=Function.prototype.call;e.exports=r?o.bind(o):function(){return o.apply(o,arguments)}},6530:(e,t,n)=>{var r=n(9781),o=n(2597),a=Function.prototype,i=r&&Object.getOwnPropertyDescriptor,s=o(a,"name"),l=s&&"something"===function(){}.name,u=s&&(!r||r&&i(a,"name").configurable);e.exports={EXISTS:s,PROPER:l,CONFIGURABLE:u}},1470:(e,t,n)=>{var r=n(4326),o=n(1702);e.exports=function(e){if("Function"===r(e))return o(e)}},1702:(e,t,n)=>{var r=n(4374),o=Function.prototype,a=o.call,i=r&&o.bind.bind(a,a);e.exports=r?i:function(e){return function(){return a.apply(e,arguments)}}},5005:(e,t,n)=>{var r=n(7854),o=n(614);e.exports=function(e,t){return arguments.length<2?(n=r[e],o(n)?n:void 0):r[e]&&r[e][t];var n}},8173:(e,t,n)=>{var r=n(9662),o=n(8554);e.exports=function(e,t){var n=e[t];return o(n)?void 0:r(n)}},647:(e,t,n)=>{var r=n(1702),o=n(7908),a=Math.floor,i=r("".charAt),s=r("".replace),l=r("".slice),u=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,c=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,n,r,p,d){var f=n+e.length,m=r.length,v=c;return void 0!==p&&(p=o(p),v=u),s(d,v,(function(o,s){var u;switch(i(s,0)){case"$":return"$";case"&":return e;case"`":return l(t,0,n);case"'":return l(t,f);case"<":u=p[l(s,1,-1)];break;default:var c=+s;if(0===c)return o;if(c>m){var d=a(c/10);return 0===d?o:d<=m?void 0===r[d-1]?i(s,1):r[d-1]+i(s,1):o}u=r[c-1]}return void 0===u?"":u}))}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||this||Function("return this")()},2597:(e,t,n)=>{var r=n(1702),o=n(7908),a=r({}.hasOwnProperty);e.exports=Object.hasOwn||function(e,t){return a(o(e),t)}},3501:e=>{e.exports={}},490:(e,t,n)=>{var r=n(5005);e.exports=r("document","documentElement")},4664:(e,t,n)=>{var r=n(9781),o=n(7293),a=n(317);e.exports=!r&&!o((function(){return 7!=Object.defineProperty(a("div"),"a",{get:function(){return 7}}).a}))},8361:(e,t,n)=>{var r=n(1702),o=n(7293),a=n(4326),i=Object,s=r("".split);e.exports=o((function(){return!i("z").propertyIsEnumerable(0)}))?function(e){return"String"==a(e)?s(e,""):i(e)}:i},2788:(e,t,n)=>{var r=n(1702),o=n(614),a=n(5465),i=r(Function.toString);o(a.inspectSource)||(a.inspectSource=function(e){return i(e)}),e.exports=a.inspectSource},9909:(e,t,n)=>{var r,o,a,i=n(4811),s=n(7854),l=n(111),u=n(8880),c=n(2597),p=n(5465),d=n(6200),f=n(3501),m="Object already initialized",v=s.TypeError,h=s.WeakMap;if(i||p.state){var g=p.state||(p.state=new h);g.get=g.get,g.has=g.has,g.set=g.set,r=function(e,t){if(g.has(e))throw v(m);return t.facade=e,g.set(e,t),t},o=function(e){return g.get(e)||{}},a=function(e){return g.has(e)}}else{var b=d("state");f[b]=!0,r=function(e,t){if(c(e,b))throw v(m);return t.facade=e,u(e,b,t),t},o=function(e){return c(e,b)?e[b]:{}},a=function(e){return c(e,b)}}e.exports={set:r,get:o,has:a,enforce:function(e){return a(e)?o(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=o(t)).type!==e)throw v("Incompatible receiver, "+e+" required");return n}}}},614:(e,t,n)=>{var r=n(4154),o=r.all;e.exports=r.IS_HTMLDDA?function(e){return"function"==typeof e||e===o}:function(e){return"function"==typeof e}},4705:(e,t,n)=>{var r=n(7293),o=n(614),a=/#|\.prototype\./,i=function(e,t){var n=l[s(e)];return n==c||n!=u&&(o(t)?r(t):!!t)},s=i.normalize=function(e){return String(e).replace(a,".").toLowerCase()},l=i.data={},u=i.NATIVE="N",c=i.POLYFILL="P";e.exports=i},8554:e=>{e.exports=function(e){return null==e}},111:(e,t,n)=>{var r=n(614),o=n(4154),a=o.all;e.exports=o.IS_HTMLDDA?function(e){return"object"==typeof e?null!==e:r(e)||e===a}:function(e){return"object"==typeof e?null!==e:r(e)}},1913:e=>{e.exports=!1},2190:(e,t,n)=>{var r=n(5005),o=n(614),a=n(7976),i=n(3307),s=Object;e.exports=i?function(e){return"symbol"==typeof e}:function(e){var t=r("Symbol");return o(t)&&a(t.prototype,s(e))}},6244:(e,t,n)=>{var r=n(7466);e.exports=function(e){return r(e.length)}},6339:(e,t,n)=>{var r=n(1702),o=n(7293),a=n(614),i=n(2597),s=n(9781),l=n(6530).CONFIGURABLE,u=n(2788),c=n(9909),p=c.enforce,d=c.get,f=String,m=Object.defineProperty,v=r("".slice),h=r("".replace),g=r([].join),b=s&&!o((function(){return 8!==m((function(){}),"length",{value:8}).length})),y=String(String).split("String"),A=e.exports=function(e,t,n){"Symbol("===v(f(t),0,7)&&(t="["+h(f(t),/^Symbol\(([^)]*)\)/,"$1")+"]"),n&&n.getter&&(t="get "+t),n&&n.setter&&(t="set "+t),(!i(e,"name")||l&&e.name!==t)&&(s?m(e,"name",{value:t,configurable:!0}):e.name=t),b&&n&&i(n,"arity")&&e.length!==n.arity&&m(e,"length",{value:n.arity});try{n&&i(n,"constructor")&&n.constructor?s&&m(e,"prototype",{writable:!1}):e.prototype&&(e.prototype=void 0)}catch(e){}var r=p(e);return i(r,"source")||(r.source=g(y,"string"==typeof t?t:"")),e};Function.prototype.toString=A((function(){return a(this)&&d(this).source||u(this)}),"toString")},4758:e=>{var t=Math.ceil,n=Math.floor;e.exports=Math.trunc||function(e){var r=+e;return(r>0?n:t)(r)}},1574:(e,t,n)=>{"use strict";var r=n(9781),o=n(1702),a=n(6916),i=n(7293),s=n(1956),l=n(5181),u=n(5296),c=n(7908),p=n(8361),d=Object.assign,f=Object.defineProperty,m=o([].concat);e.exports=!d||i((function(){if(r&&1!==d({b:1},d(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach((function(e){t[e]=e})),7!=d({},e)[n]||s(d({},t)).join("")!=o}))?function(e,t){for(var n=c(e),o=arguments.length,i=1,d=l.f,f=u.f;o>i;)for(var v,h=p(arguments[i++]),g=d?m(s(h),d(h)):s(h),b=g.length,y=0;b>y;)v=g[y++],r&&!a(f,h,v)||(n[v]=h[v]);return n}:d},30:(e,t,n)=>{var r,o=n(9670),a=n(6048),i=n(748),s=n(3501),l=n(490),u=n(317),c=n(6200),p="prototype",d="script",f=c("IE_PROTO"),m=function(){},v=function(e){return"<"+d+">"+e+""},h=function(e){e.write(v("")),e.close();var t=e.parentWindow.Object;return e=null,t},g=function(){try{r=new ActiveXObject("htmlfile")}catch(e){}var e,t,n;g="undefined"!=typeof document?document.domain&&r?h(r):(t=u("iframe"),n="java"+d+":",t.style.display="none",l.appendChild(t),t.src=String(n),(e=t.contentWindow.document).open(),e.write(v("document.F=Object")),e.close(),e.F):h(r);for(var o=i.length;o--;)delete g[p][i[o]];return g()};s[f]=!0,e.exports=Object.create||function(e,t){var n;return null!==e?(m[p]=o(e),n=new m,m[p]=null,n[f]=e):n=g(),void 0===t?n:a.f(n,t)}},6048:(e,t,n)=>{var r=n(9781),o=n(3353),a=n(3070),i=n(9670),s=n(5656),l=n(1956);t.f=r&&!o?Object.defineProperties:function(e,t){i(e);for(var n,r=s(t),o=l(t),u=o.length,c=0;u>c;)a.f(e,n=o[c++],r[n]);return e}},3070:(e,t,n)=>{var r=n(9781),o=n(4664),a=n(3353),i=n(9670),s=n(4948),l=TypeError,u=Object.defineProperty,c=Object.getOwnPropertyDescriptor,p="enumerable",d="configurable",f="writable";t.f=r?a?function(e,t,n){if(i(e),t=s(t),i(n),"function"==typeof e&&"prototype"===t&&"value"in n&&f in n&&!n[f]){var r=c(e,t);r&&r[f]&&(e[t]=n.value,n={configurable:d in n?n[d]:r[d],enumerable:p in n?n[p]:r[p],writable:!1})}return u(e,t,n)}:u:function(e,t,n){if(i(e),t=s(t),i(n),o)try{return u(e,t,n)}catch(e){}if("get"in n||"set"in n)throw l("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},1236:(e,t,n)=>{var r=n(9781),o=n(6916),a=n(5296),i=n(9114),s=n(5656),l=n(4948),u=n(2597),c=n(4664),p=Object.getOwnPropertyDescriptor;t.f=r?p:function(e,t){if(e=s(e),t=l(t),c)try{return p(e,t)}catch(e){}if(u(e,t))return i(!o(a.f,e,t),e[t])}},8006:(e,t,n)=>{var r=n(6324),o=n(748).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return r(e,o)}},5181:(e,t)=>{t.f=Object.getOwnPropertySymbols},7976:(e,t,n)=>{var r=n(1702);e.exports=r({}.isPrototypeOf)},6324:(e,t,n)=>{var r=n(1702),o=n(2597),a=n(5656),i=n(1318).indexOf,s=n(3501),l=r([].push);e.exports=function(e,t){var n,r=a(e),u=0,c=[];for(n in r)!o(s,n)&&o(r,n)&&l(c,n);for(;t.length>u;)o(r,n=t[u++])&&(~i(c,n)||l(c,n));return c}},1956:(e,t,n)=>{var r=n(6324),o=n(748);e.exports=Object.keys||function(e){return r(e,o)}},5296:(e,t)=>{"use strict";var n={}.propertyIsEnumerable,r=Object.getOwnPropertyDescriptor,o=r&&!n.call({1:2},1);t.f=o?function(e){var t=r(this,e);return!!t&&t.enumerable}:n},288:(e,t,n)=>{"use strict";var r=n(1694),o=n(648);e.exports=r?{}.toString:function(){return"[object "+o(this)+"]"}},2140:(e,t,n)=>{var r=n(6916),o=n(614),a=n(111),i=TypeError;e.exports=function(e,t){var n,s;if("string"===t&&o(n=e.toString)&&!a(s=r(n,e)))return s;if(o(n=e.valueOf)&&!a(s=r(n,e)))return s;if("string"!==t&&o(n=e.toString)&&!a(s=r(n,e)))return s;throw i("Can't convert object to primitive value")}},3887:(e,t,n)=>{var r=n(5005),o=n(1702),a=n(8006),i=n(5181),s=n(9670),l=o([].concat);e.exports=r("Reflect","ownKeys")||function(e){var t=a.f(s(e)),n=i.f;return n?l(t,n(e)):t}},7651:(e,t,n)=>{var r=n(6916),o=n(9670),a=n(614),i=n(4326),s=n(2261),l=TypeError;e.exports=function(e,t){var n=e.exec;if(a(n)){var u=r(n,e,t);return null!==u&&o(u),u}if("RegExp"===i(e))return r(s,e,t);throw l("RegExp#exec called on incompatible receiver")}},2261:(e,t,n)=>{"use strict";var r,o,a=n(6916),i=n(1702),s=n(1340),l=n(7066),u=n(2999),c=n(2309),p=n(30),d=n(9909).get,f=n(9441),m=n(7168),v=c("native-string-replace",String.prototype.replace),h=RegExp.prototype.exec,g=h,b=i("".charAt),y=i("".indexOf),A=i("".replace),w=i("".slice),C=(o=/b*/g,a(h,r=/a/,"a"),a(h,o,"a"),0!==r.lastIndex||0!==o.lastIndex),x=u.BROKEN_CARET,k=void 0!==/()??/.exec("")[1];(C||k||x||f||m)&&(g=function(e){var t,n,r,o,i,u,c,f=this,m=d(f),_=s(e),S=m.raw;if(S)return S.lastIndex=f.lastIndex,t=a(g,S,_),f.lastIndex=S.lastIndex,t;var O=m.groups,E=x&&f.sticky,T=a(l,f),P=f.source,j=0,N=_;if(E&&(T=A(T,"y",""),-1===y(T,"g")&&(T+="g"),N=w(_,f.lastIndex),f.lastIndex>0&&(!f.multiline||f.multiline&&"\n"!==b(_,f.lastIndex-1))&&(P="(?: "+P+")",N=" "+N,j++),n=new RegExp("^(?:"+P+")",T)),k&&(n=new RegExp("^"+P+"$(?!\\s)",T)),C&&(r=f.lastIndex),o=a(h,E?n:f,N),E?o?(o.input=w(o.input,j),o[0]=w(o[0],j),o.index=f.lastIndex,f.lastIndex+=o[0].length):f.lastIndex=0:C&&o&&(f.lastIndex=f.global?o.index+o[0].length:r),k&&o&&o.length>1&&a(v,o[0],n,(function(){for(i=1;i{"use strict";var r=n(9670);e.exports=function(){var e=r(this),t="";return e.hasIndices&&(t+="d"),e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.unicodeSets&&(t+="v"),e.sticky&&(t+="y"),t}},4706:(e,t,n)=>{var r=n(6916),o=n(2597),a=n(7976),i=n(7066),s=RegExp.prototype;e.exports=function(e){var t=e.flags;return void 0!==t||"flags"in s||o(e,"flags")||!a(s,e)?t:r(i,e)}},2999:(e,t,n)=>{var r=n(7293),o=n(7854).RegExp,a=r((function(){var e=o("a","y");return e.lastIndex=2,null!=e.exec("abcd")})),i=a||r((function(){return!o("a","y").sticky})),s=a||r((function(){var e=o("^r","gy");return e.lastIndex=2,null!=e.exec("str")}));e.exports={BROKEN_CARET:s,MISSED_STICKY:i,UNSUPPORTED_Y:a}},9441:(e,t,n)=>{var r=n(7293),o=n(7854).RegExp;e.exports=r((function(){var e=o(".","s");return!(e.dotAll&&e.exec("\n")&&"s"===e.flags)}))},7168:(e,t,n)=>{var r=n(7293),o=n(7854).RegExp;e.exports=r((function(){var e=o("(?b)","g");return"b"!==e.exec("b").groups.a||"bc"!=="b".replace(e,"$c")}))},4488:(e,t,n)=>{var r=n(8554),o=TypeError;e.exports=function(e){if(r(e))throw o("Can't call method on "+e);return e}},6200:(e,t,n)=>{var r=n(2309),o=n(9711),a=r("keys");e.exports=function(e){return a[e]||(a[e]=o(e))}},5465:(e,t,n)=>{var r=n(7854),o=n(3072),a="__core-js_shared__",i=r[a]||o(a,{});e.exports=i},2309:(e,t,n)=>{var r=n(1913),o=n(5465);(e.exports=function(e,t){return o[e]||(o[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.30.2",mode:r?"pure":"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.30.2/LICENSE",source:"https://github.com/zloirock/core-js"})},8710:(e,t,n)=>{var r=n(1702),o=n(9303),a=n(1340),i=n(4488),s=r("".charAt),l=r("".charCodeAt),u=r("".slice),c=function(e){return function(t,n){var r,c,p=a(i(t)),d=o(n),f=p.length;return d<0||d>=f?e?"":void 0:(r=l(p,d))<55296||r>56319||d+1===f||(c=l(p,d+1))<56320||c>57343?e?s(p,d):r:e?u(p,d,d+2):c-56320+(r-55296<<10)+65536}};e.exports={codeAt:c(!1),charAt:c(!0)}},6293:(e,t,n)=>{var r=n(7392),o=n(7293),a=n(7854).String;e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!a(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},1400:(e,t,n)=>{var r=n(9303),o=Math.max,a=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):a(n,t)}},5656:(e,t,n)=>{var r=n(8361),o=n(4488);e.exports=function(e){return r(o(e))}},9303:(e,t,n)=>{var r=n(4758);e.exports=function(e){var t=+e;return t!=t||0===t?0:r(t)}},7466:(e,t,n)=>{var r=n(9303),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},7908:(e,t,n)=>{var r=n(4488),o=Object;e.exports=function(e){return o(r(e))}},7593:(e,t,n)=>{var r=n(6916),o=n(111),a=n(2190),i=n(8173),s=n(2140),l=n(5112),u=TypeError,c=l("toPrimitive");e.exports=function(e,t){if(!o(e)||a(e))return e;var n,l=i(e,c);if(l){if(void 0===t&&(t="default"),n=r(l,e,t),!o(n)||a(n))return n;throw u("Can't convert object to primitive value")}return void 0===t&&(t="number"),s(e,t)}},4948:(e,t,n)=>{var r=n(7593),o=n(2190);e.exports=function(e){var t=r(e,"string");return o(t)?t:t+""}},1694:(e,t,n)=>{var r={};r[n(5112)("toStringTag")]="z",e.exports="[object z]"===String(r)},1340:(e,t,n)=>{var r=n(648),o=String;e.exports=function(e){if("Symbol"===r(e))throw TypeError("Cannot convert a Symbol value to a string");return o(e)}},6330:e=>{var t=String;e.exports=function(e){try{return t(e)}catch(e){return"Object"}}},9711:(e,t,n)=>{var r=n(1702),o=0,a=Math.random(),i=r(1..toString);e.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+i(++o+a,36)}},3307:(e,t,n)=>{var r=n(6293);e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},3353:(e,t,n)=>{var r=n(9781),o=n(7293);e.exports=r&&o((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},4811:(e,t,n)=>{var r=n(7854),o=n(614),a=r.WeakMap;e.exports=o(a)&&/native code/.test(String(a))},5112:(e,t,n)=>{var r=n(7854),o=n(2309),a=n(2597),i=n(9711),s=n(6293),l=n(3307),u=r.Symbol,c=o("wks"),p=l?u.for||u:u&&u.withoutSetter||i;e.exports=function(e){return a(c,e)||(c[e]=s&&a(u,e)?u[e]:p("Symbol."+e)),c[e]}},2772:(e,t,n)=>{"use strict";var r=n(2109),o=n(1470),a=n(1318).indexOf,i=n(9341),s=o([].indexOf),l=!!s&&1/s([1],1,-0)<0;r({target:"Array",proto:!0,forced:l||!i("indexOf")},{indexOf:function(e){var t=arguments.length>1?arguments[1]:void 0;return l?s(this,e,t)||0:a(this,e,t)}})},9601:(e,t,n)=>{var r=n(2109),o=n(1574);r({target:"Object",stat:!0,arity:2,forced:Object.assign!==o},{assign:o})},9070:(e,t,n)=>{var r=n(2109),o=n(9781),a=n(3070).f;r({target:"Object",stat:!0,forced:Object.defineProperty!==a,sham:!o},{defineProperty:a})},1539:(e,t,n)=>{var r=n(1694),o=n(8052),a=n(288);r||o(Object.prototype,"toString",a,{unsafe:!0})},4916:(e,t,n)=>{"use strict";var r=n(2109),o=n(2261);r({target:"RegExp",proto:!0,forced:/./.exec!==o},{exec:o})},9714:(e,t,n)=>{"use strict";var r=n(6530).PROPER,o=n(8052),a=n(9670),i=n(1340),s=n(7293),l=n(4706),u="toString",c=RegExp.prototype[u],p=s((function(){return"/a/b"!=c.call({source:"a",flags:"b"})})),d=r&&c.name!=u;(p||d)&&o(RegExp.prototype,u,(function(){var e=a(this);return"/"+i(e.source)+"/"+i(l(e))}),{unsafe:!0})},5306:(e,t,n)=>{"use strict";var r=n(2104),o=n(6916),a=n(1702),i=n(7007),s=n(7293),l=n(9670),u=n(614),c=n(8554),p=n(9303),d=n(7466),f=n(1340),m=n(4488),v=n(1530),h=n(8173),g=n(647),b=n(7651),y=n(5112)("replace"),A=Math.max,w=Math.min,C=a([].concat),x=a([].push),k=a("".indexOf),_=a("".slice),S="$0"==="a".replace(/./,"$0"),O=!!/./[y]&&""===/./[y]("a","$0");i("replace",(function(e,t,n){var a=O?"$":"$0";return[function(e,n){var r=m(this),a=c(e)?void 0:h(e,y);return a?o(a,e,r,n):o(t,f(r),e,n)},function(e,o){var i=l(this),s=f(e);if("string"==typeof o&&-1===k(o,a)&&-1===k(o,"$<")){var c=n(t,i,s,o);if(c.done)return c.value}var m=u(o);m||(o=f(o));var h=i.global;if(h){var y=i.unicode;i.lastIndex=0}for(var S=[];;){var O=b(i,s);if(null===O)break;if(x(S,O),!h)break;""===f(O[0])&&(i.lastIndex=v(s,d(i.lastIndex),y))}for(var E,T="",P=0,j=0;j=P&&(T+=_(s,P,D)+L,P=D+N.length)}return T+_(s,P)}]}),!!s((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")}))||!S||O)},2204:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s});var r=n(7537),o=n.n(r),a=n(3645),i=n.n(a)()(o());i.push([e.id,"\n.faces-list[data-v-05fcaf74] {\n\tpadding: 10px 0 15px;\n}\n.face-entry[data-v-05fcaf74] {\n\tdisplay: flex;\n\talign-items: center;\n\tmargin-bottom: 8px;\n}\n.face-name[data-v-05fcaf74] {\n\twidth: 100%;\n\tpadding: 8px;\n}\n.unknown-name[data-v-05fcaf74] {\n\topacity: .7;\n}\n.face-preview[data-v-05fcaf74] {\n\tbackground-color: rgba(210, 210, 210, .75);\n\tborder-radius: 50%;\n\theight: 48px;\n\twidth: 48px;\n}\n.face-preview.unknown-name[data-v-05fcaf74]:hover {\n\topacity: 1;\n}\n.icon-action[data-v-05fcaf74] {\n\tmin-width: 36px;\n\tmin-height: 36px;\n\tborder-radius: 18px;\n\topacity: 0.7;\n}\n.icon-action[data-v-05fcaf74]:hover {\n\topacity: 1;\n\tbackground-color: rgba(127,127,127,.25) !important;\n}\n\n","",{version:3,sources:["webpack://./src/views/PersonRow.vue"],names:[],mappings:";AAwJA;CACA,oBAAA;AACA;AAEA;CACA,aAAA;CACA,mBAAA;CACA,kBAAA;AACA;AAEA;CACA,WAAA;CACA,YAAA;AACA;AAEA;CACA,WAAA;AACA;AAEA;CACA,0CAAA;CACA,kBAAA;CACA,YAAA;CACA,WAAA;AACA;AAEA;CACA,UAAA;AACA;AAEA;CACA,eAAA;CACA,gBAAA;CACA,mBAAA;CACA,YAAA;AACA;AAEA;CACA,UAAA;CACA,kDAAA;AACA",sourcesContent:['\x3c!--\n - @copyright Copyright (c) 2020 Matias De lellis \n -\n - @author Matias De lellis \n -\n - @license GNU AGPL version 3 or any later version\n -\n - This program is free software: you can redistribute it and/or modify\n - it under the terms of the GNU Affero General Public License as\n - published by the Free Software Foundation, either version 3 of the\n - License, or (at your option) any later version.\n -\n - This program is distributed in the hope that it will be useful,\n - but WITHOUT ANY WARRANTY; without even the implied warranty of\n - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n - GNU Affero General Public License for more details.\n -\n - You should have received a copy of the GNU Affero General Public License\n - along with this program. If not, see .\n -\n --\x3e\n\n\n + + \ No newline at end of file diff --git a/git_facerecognition/src/views/PersonsTab.vue b/git_facerecognition/src/views/PersonsTab.vue new file mode 100644 index 0000000..5a8d431 --- /dev/null +++ b/git_facerecognition/src/views/PersonsTab.vue @@ -0,0 +1,293 @@ + + + + + \ No newline at end of file diff --git a/git_facerecognition/templates/settings/admin.php b/git_facerecognition/templates/settings/admin.php new file mode 100644 index 0000000..4f61207 --- /dev/null +++ b/git_facerecognition/templates/settings/admin.php @@ -0,0 +1,89 @@ + + +
+
+

+ t('Face Recognition'));?> + +

+

+ t('Temporary files'));?> +

+

t('During analysis, temporary files are used to ensure homogeneity between all images.'));?>

+

t('Small images allow a quick analysis, but you can lose the smallest faces of your photos. Large images can improve the results, but the analysis will be slower.'));?> + +

+

+ + disabled> + + ... + + +

+
+

+ t('Clustering threshold'));?> +

+

t('Persons are determined as groups of similar faces and to obtain them, all the faces found must be compared. When they are compared, a threshold is used to determine if they should be grouped.'));?>

+

t('A small threshold will only group very similar faces, but initially you will have many groups to name. A larger threshold is more flexible to group the faces and obtaining fewer groups, but being able to confuse similar persons.'));?> + +

+

+ + disabled> + + ... + + +

+
+

+ t('Minimum confidence'));?> +

+

t('The minimum confidence determines how reliable must be a face detection to try to group it. Blurred or misaligned faces would have a confidence closer to 0.0, and the best images close to 1.0.'));?> + +

+

+ + disabled> + + ... + + +

+
+

+ t('Minimum of faces in cluster'));?> +

+

t('The minimum number of faces that a cluster must have to display it to the user.'));?>

+

t('These faces clusters will not be shown as a suggestion, but can always be renamed eventually in the side panel.'));?>

+

+ + disabled> + + ... + + +

+
+

+ t('Configuration information'));?> + +

+

t('Current model:'));?>

+

t('Maximum memory assigned for image processing:'));?>

+

+
+

+ t('Current status'));?> +

+
+

t('Stopped'));?>

+ +
+
+
diff --git a/git_facerecognition/templates/settings/personal.php b/git_facerecognition/templates/settings/personal.php new file mode 100644 index 0000000..8bedffa --- /dev/null +++ b/git_facerecognition/templates/settings/personal.php @@ -0,0 +1,12 @@ + +
diff --git a/git_facerecognition/tests/Integration/AddMissingImagesTaskTest.php b/git_facerecognition/tests/Integration/AddMissingImagesTaskTest.php new file mode 100644 index 0000000..87159b8 --- /dev/null +++ b/git_facerecognition/tests/Integration/AddMissingImagesTaskTest.php @@ -0,0 +1,138 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; + +use OCA\FaceRecognition\Model\ModelManager; + +class AddMissingImagesTaskTest extends IntegrationTestCase { + + /** + * Test that AddMissingImagesTask is updating app config that it finished full scan. + * Note that, in this test, we cannot check number of newly found images, + * as this instance might be in use and can lead to wrong results + */ + public function testFinishedFullScan() { + $this->doMissingImageScan(); + + $fullImageScanDone = $this->config->getUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + $this->assertEquals('true', $fullImageScanDone); + } + + /** + * Test that, after one scan is done, next scan will not find any new images + */ + public function testNewScanIsEmpty() { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + + // Do it once, to make sure all images are inserted + $this->doMissingImageScan(); + $fullImageScanDone = $this->config->getUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + $this->assertEquals('true', $fullImageScanDone); + + // Second time, there should be no newly inserted images + $this->doMissingImageScan(); + + $this->assertEquals(0, $this->context->propertyBag['AddMissingImagesTask_insertedImages']); + $this->assertEquals(0, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + } + + /** + * Test that empty crawling will do nothing + */ + public function testCrawlNoImages() { + $this->loginAsUser($this->user->getUID()); + $view = new View('/' . $this->user->getUID() . '/files'); + $view->file_put_contents("foo.txt", "content"); + + $this->doMissingImageScan($this->user); + + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $this->assertEquals(0, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + $this->assertEquals(0, $this->context->propertyBag['AddMissingImagesTask_insertedImages']); + } + + /** + * Test that crawling with some images will actually find them and add them to database + */ + public function testCrawl() { + $this->loginAsUser($this->user->getUID()); + $view = new View('/' . $this->user->getUID() . '/files'); + $view->file_put_contents("foo1.txt", "content"); + $view->file_put_contents("foo2.jpg", "content"); + $view->file_put_contents("foo3.png", "content"); + $view->mkdir('dir'); + $view->file_put_contents("dir/foo4.txt", "content"); + $view->file_put_contents("dir/foo5.bmp", "content"); + $view->file_put_contents("dir/foo6.png", "content"); + $view->mkdir('dir_nomedia'); + $view->file_put_contents("dir_nomedia/.nomedia", "content"); + $view->file_put_contents("dir_nomedia/foo7.jpg", "content"); + + $this->doMissingImageScan($this->user); + + // We should find 3 images only - foo2.jpg, foo3.png and dir/foo6.png. BMP mimetype (foo5.bmp) is not enabled by default. + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $this->assertEquals(3, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + $this->assertEquals(3, $this->context->propertyBag['AddMissingImagesTask_insertedImages']); + } + + /** + * Helper method to set up and do scanning + * + * @param IUser|null $contextUser Optional user to scan for. If not given, images for all users will be scanned. + */ + private function doMissingImageScan($contextUser = null) { + // Reset config that full scan is done, to make sure we are scanning again + $this->config->setUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $addMissingImagesTask = new AddMissingImagesTask($imageMapper, $fileService, $settingsService); + $this->assertNotEquals("", $addMissingImagesTask->description()); + + // Set user for which to do scanning, if any + $this->context->user = $contextUser; + + // Since this task returns generator, iterate until it is done + $generator = $addMissingImagesTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/AppTest.php b/git_facerecognition/tests/Integration/AppTest.php new file mode 100644 index 0000000..5bd907f --- /dev/null +++ b/git_facerecognition/tests/Integration/AppTest.php @@ -0,0 +1,41 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +class AppTest extends IntegrationTestCase { + + public function setUp(): void { + parent::setUp(); + $app = new App('facerecognition'); + $this->container = $app->getContainer(); + } + + public function testAppInstalled() { + $appManager = $this->container->query('OCP\App\IAppManager'); + $this->assertTrue($appManager->isInstalled('facerecognition')); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/CreateClustersTaskTest.php b/git_facerecognition/tests/Integration/CreateClustersTaskTest.php new file mode 100644 index 0000000..c03da26 --- /dev/null +++ b/git_facerecognition/tests/Integration/CreateClustersTaskTest.php @@ -0,0 +1,118 @@ + + * @copyright Copyright (c) 2019, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\CreateClustersTask; +use OCA\FaceRecognition\Db\Face; +use OCA\FaceRecognition\Db\Image; +use OCA\FaceRecognition\Model\ModelManager; + +use Test\TestCase; + +class CreateClustersTaskTest extends IntegrationTestCase { + + /** + * Test that one face that was not in any cluster will be assigned new person + */ + public function testCreateSingleFaceCluster() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + + $image = new Image(); + $image->setUser($this->user->getUid()); + $image->setFile(1); + $image->setModel(ModelManager::DEFAULT_FACE_MODEL_ID); + $imageMapper->insert($image); + + $face = Face::fromModel($image->getId(), array("left"=>0, "right"=>100, "top"=>0, "bottom"=>100, "detection_confidence"=>1.0)); + $faceMapper->insertFace($face); + + // With a single face should never create clusters. + $this->doCreateClustersTask($personMapper, $imageMapper, $faceMapper, $settingsService, $this->user); + + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $personCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, count($persons)); + + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $faceCount); + $faces = $faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($faces)); + $this->assertNull($faces[0]->getPerson()); + + // Force clustering the sigle face. + $settingsService->_setForceCreateClusters(true, $this->user->getUID()); + + $this->doCreateClustersTask($personMapper, $imageMapper, $faceMapper, $settingsService, $this->user); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $clusterCount); + + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($persons)); + $personId = $persons[0]->getId(); + + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $faceCount); + + $faces = $faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($faces)); + $this->assertNotNull($faces[0]->getPerson()); + + $faces = $faceMapper->findFromCluster($this->user->getUID(), $personId, ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($faces)); + } + + /** + * Helper method to set up and do create clusters task + * + * @param IUser|null $contextUser Optional user to create clusters for. + * If not given, clusters for all users will be processed. + */ + private function doCreateClustersTask($personMapper, $imageMapper, $faceMapper, $settingsService, $contextUser = null) { + $createClustersTask = new CreateClustersTask($personMapper, $imageMapper, $faceMapper, $settingsService); + $this->assertNotEquals("", $createClustersTask->description()); + + // Set user for which to do processing, if any + $this->context->user = $contextUser; + + // Since this task returns generator, iterate until it is done + $generator = $createClustersTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/DisabledUserRemovalTaskTest.php b/git_facerecognition/tests/Integration/DisabledUserRemovalTaskTest.php new file mode 100644 index 0000000..a9bbc17 --- /dev/null +++ b/git_facerecognition/tests/Integration/DisabledUserRemovalTaskTest.php @@ -0,0 +1,114 @@ + + * @copyright Copyright (c) 2018-2019, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; +use OCA\FaceRecognition\BackgroundJob\Tasks\DisabledUserRemovalTask; + +use OCA\FaceRecognition\Db\Image; + +use OCA\FaceRecognition\Model\ModelManager; + +use Test\TestCase; + +class DisabledUserRemovalTaskTest extends IntegrationTestCase { + + /** + * Test that check when user disable analysis. + */ + public function testNoMediaImageRemoval() { + // Enables the analysis for the user to add images. + $this->config->setUserValue($this->user->getUID(), 'facerecognition', 'enabled', 'true'); + + // Create foo1.jpg in root and foo2.jpg in child directory + $view = new View('/' . $this->user->getUID() . '/files'); + $view->file_put_contents("foo1.jpg", "content"); + $view->mkdir('dir_nomedia'); + $view->file_put_contents("dir_nomedia/foo2.jpg", "content"); + + // Create these two images in database by calling add missing images task + $this->config->setUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $addMissingImagesTask = new AddMissingImagesTask($imageMapper, $fileService, $settingsService); + $this->context->user = $this->user; + $generator = $addMissingImagesTask->execute($this->context); + foreach ($generator as $_) { + } + + // TODO: add faces and person for those images, so we can exercise person + // invalidation and face removal when image is removed. + + // We should find 2 images now - foo1.jpg, foo2.png + $this->assertEquals(2, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + + // Disable analysis for user + $this->config->setUserValue($this->user->getUID(), 'facerecognition', 'enabled', 'false'); + + // Perform the removal due user disabling action. + $this->doDisabledUserRemoval(); + + // Now it must be empty + $this->assertEquals(0, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + } + + /** + * Helper method to set up and do removal task. + * + * @param IUser|null $contextUser Optional user to scan for. + * If not given, stale images for all users will be renived. + */ + private function doDisabledUserRemoval($contextUser = null) { + $disabledUserRemovalTask = $this->createDisabledUserRemovalTask(); + $this->assertNotEquals("", $disabledUserRemovalTask->description()); + + // Set user for which to do scanning, if any + $this->context->user = $contextUser; + + // Since this task returns generator, iterate until it is done + $generator = $disabledUserRemovalTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } + + private function createDisabledUserRemovalTask() { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $faceMgmtService = $this->container->query('OCA\FaceRecognition\Service\FaceManagementService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + return new DisabledUserRemovalTask($imageMapper, $faceMgmtService, $settingsService); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/ImageProcessingTaskTest.php b/git_facerecognition/tests/Integration/ImageProcessingTaskTest.php new file mode 100644 index 0000000..fe89767 --- /dev/null +++ b/git_facerecognition/tests/Integration/ImageProcessingTaskTest.php @@ -0,0 +1,206 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; +use OCA\FaceRecognition\BackgroundJob\Tasks\ImageProcessingTask; +use OCA\FaceRecognition\Db\Image; +use OCA\FaceRecognition\Model\ModelManager; + +use Test\TestCase; + +class ImageProcessingTaskTest extends IntegrationTestCase { + + public function setUp(): void { + parent::setUp(); + + // Since test is changing this values, try to preserve old values (this is best effort) + $this->originalMinImageSize = intval($this->config->getAppValue('facerecognition', 'min_image_size', '512')); + $this->originalMaxImageArea = intval($this->config->getAppValue('facerecognition', 'max_image_area', 0)); + $this->config->setAppValue('facerecognition', 'min_image_size', 1); + $this->config->setAppValue('facerecognition', 'max_image_area', 200 * 200); + + // Install models needed to test + $model = $this->container->query('OCA\FaceRecognition\Model\DlibCnnModel\DlibCnn5Model'); + $model->install(); + + } + + public function tearDown(): void { + $this->config->setAppValue('facerecognition', 'min_image_size', $this->originalMinImageSize); + $this->config->setAppValue('facerecognition', 'max_image_area', $this->originalMaxImageArea); + + parent::tearDown(); + } + + /** + * Tests when image cannot be loaded at all + * (tests whether image is declared as processed and error is added to it) + */ + public function testInvalidImage() { + $image = $this->genericTestImageProcessing('bogus image data', true, 0); + // Invalid image should have 0 as processing duration + $this->assertEquals(0, $image->getProcessingDuration()); + } + + /** + * Tests that small images are skipped during processing + */ + public function testImageTooSmallToProcess() { + $this->config->setAppValue('facerecognition', 'min_image_size', 10000); + $imgData = file_get_contents(\OC::$SERVERROOT . '/apps/facerecognition/tests/assets/lenna.jpg'); + $image = $this->genericTestImageProcessing($imgData, false, 0); + } + + /** + * Test when there is no faces on image + * (image should be declared as processed, but 0 faces should be associated with it) + */ + public function testNoFacesFound() { + $imgData = file_get_contents(\OC::$SERVERROOT . '/apps/facerecognition/tests/assets/black.jpg'); + $image = $this->genericTestImageProcessing($imgData, false, 0); + } + + /** + * Regular positive test that find one face in image + */ + public function testFindFace() { + $imgData = file_get_contents(\OC::$SERVERROOT . '/apps/facerecognition/tests/assets/lenna.jpg'); + $image = $this->genericTestImageProcessing($imgData, false, 1); + + // Check exact values for face boundaries (might need to update when we bump dlib/pdlib versions) + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $face = $faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID)[0]; + $face = $faceMapper->find($face->getId()); + $this->assertEquals(49, $face->getX()); + $this->assertEquals(62, $face->getY()); + $this->assertEquals(75, $face->getWidth()); + $this->assertEquals(75, $face->getHeight()); + } + + /** + * Helper function that asserts in generic fashion whatever necessary. + * + * @param string|resource $imgData Image data that will be analyzed + * @param bool $expectingError True if we should assert that error is found, false if we should assert there is no error + * @param int $expectedFacesCount Number of faces that we should assert that should be found in processed image + * + * @return Image One found image + */ + private function genericTestImageProcessing($imgData, $expectingError, $expectedFacesCount) { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $this->doImageProcessing($imgData); + + // Check that there is no unprocessed images + $this->assertEquals(0, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + + // Check image fields after processing + $images = $imageMapper->findImages($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($images)); + $image = $imageMapper->find($this->user->getUID(), $images[0]->getId()); + $this->assertTrue(is_null($image->getError()) xor $expectingError); + $this->assertTrue($image->getIsProcessed()); + $this->assertNotNull(0, $image->getProcessingDuration()); + $this->assertNotNull($image->getLastProcessedTime()); + + // Check number of found faces + $this->assertEquals($expectedFacesCount, count($faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID))); + + return $image; + } + + /** + * Helper method to set up and do image processing + * + * @param string|resource $imgData Image data that will be analyzed + * @param IUser|null $contextUser Optional user to process images for. + * If not given, images for all users will be processed. + */ + private function doImageProcessing($imgData, $contextUser = null) { + // Create ImageProcessingTask + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $modelManager = $this->container->query('OCA\FaceRecognition\Model\ModelManager'); + $lockingProvider = $this->container->query('OCP\Lock\ILockingProvider'); + $imageProcessingTask = new ImageProcessingTask($imageMapper, $fileService, $settingsService, $modelManager, $lockingProvider); + $this->assertNotEquals("", $imageProcessingTask->description()); + + // Set user for which to do processing, if any + $this->context->user = $contextUser; + // Upload file + $this->loginAsUser($this->user->getUID()); + $view = new View('/' . $this->user->getUID() . '/files'); + $view->file_put_contents("foo1.jpg", $imgData); + // Scan it, so it is in database, ready to be processed + $this->doMissingImageScan($this->user); + $this->context->propertyBag['images'] = $imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($this->context->propertyBag['images'])); + + // Since this task returns generator, iterate until it is done + $generator = $imageProcessingTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } + + /** + * Helper method to set up and do scanning + * + * @param IUser|null $contextUser Optional user to scan for. If not given, images for all users will be scanned. + */ + private function doMissingImageScan($contextUser = null) { + // Reset config that full scan is done, to make sure we are scanning again + $this->config->setUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $addMissingImagesTask = new AddMissingImagesTask($imageMapper, $fileService, $settingsService); + + // Set user for which to do scanning, if any + $this->context->user = $contextUser; + + // Since this task returns generator, iterate until it is done + $generator = $addMissingImagesTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/IntegrationTestCase.php b/git_facerecognition/tests/Integration/IntegrationTestCase.php new file mode 100644 index 0000000..5644c77 --- /dev/null +++ b/git_facerecognition/tests/Integration/IntegrationTestCase.php @@ -0,0 +1,91 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; + +use Test\TestCase; + +/** + * Main class that all integration tests should inherit from. + */ +abstract class IntegrationTestCase extends TestCase { + /** @var IAppContainer */ + protected $container; + + /** @var FaceRecognitionContext Context */ + protected $context; + + /** @var IUser User */ + protected $user; + + /** @var IConfig Config */ + protected $config; + + public function setUp(): void { + parent::setUp(); + // Better safe than sorry. Warn user that database will be changed in chaotic manner:) + if (false === getenv('TRAVIS')) { + $this->fail("This test touches database. Add \"TRAVIS\" env variable if you want to run these test on your local instance."); + } + + // Create user on which we will upload images and do testing + $userManager = OC::$server->getUserManager(); + $username = 'testuser' . rand(0, PHP_INT_MAX); + $this->user = $userManager->createUser($username, 'password'); + $this->loginAsUser($username); + // Get container to get classes using DI + $app = new App('facerecognition'); + $this->container = $app->getContainer(); + + // Insantiate our context, that all tasks need + $userManager = $this->container->query('OCP\IUserManager'); + $this->config = $this->container->query('OCP\IConfig'); + $this->context = new FaceRecognitionContext($userManager, $this->config); + $logger = $this->container->query('Psr\Log\LoggerInterface'); + $this->context->logger = new FaceRecognitionLogger($logger); + + // The tests, by default, are with the analysis activated. + $this->config->setUserValue($this->user->getUID(), 'facerecognition', 'enabled', 'true'); + } + + public function tearDown(): void { + $faceMgmtService = $this->container->query('OCA\FaceRecognition\Service\FaceManagementService'); + $faceMgmtService->resetAllForUser($this->user->getUID()); + + $this->user->delete(); + + parent::tearDown(); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/MergeClusterToDatabaseTest.php b/git_facerecognition/tests/Integration/MergeClusterToDatabaseTest.php new file mode 100644 index 0000000..730630e --- /dev/null +++ b/git_facerecognition/tests/Integration/MergeClusterToDatabaseTest.php @@ -0,0 +1,690 @@ + + * @copyright Copyright (c) 2019, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\Db\DoesNotExistException; + +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\CreateClustersTask; +use OCA\FaceRecognition\Db\Face; +use OCA\FaceRecognition\Db\Image; +use OCA\FaceRecognition\Db\Person; +use OCA\FaceRecognition\Model\ModelManager; + +use Test\TestCase; + +class MergeClusterToDatabaseTest extends IntegrationTestCase { + + public function testMergeEmptyClusterToDatabase() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $personMapper->mergeClusterToDatabase($this->user->getUid(), array(), array()); + + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $personCount); + } + + /** + * Test when [] changes to p1=>[f1] + * (test that new person is created) + */ + public function testCreatePerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + + $image = $this->createImage(); + $face = $this->createFace($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), array(), array(100=>[$face->getId()])); + + $personId = $this->assertOnePerson(); + $this->assertFaces([$personId => [$face->getId()]]); + } + + /** + * Test when p1=>[f1] changes to p1=>[f1] + * (test that nothing happens when input and output clusters are the same) + */ + public function testSamePerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + + $person = $this->createPerson(); + $image = $this->createImage(); + $face = $this->createFace($image->getId(), $person->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array($person->getId() => [$face->getId()]), + array($person->getId() => [$face->getId()])); + + $personId = $this->assertOnePerson("foo"); + $this->assertFaces([$personId => [$face->getId()]]); + } + + /** + * Test when p1=>[f1] changes to p2=>[f1] + * (test that new person is created and old one deleted when face changes person) + */ + public function testChangePerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + + $person = $this->createPerson(); + $image = $this->createImage(); + $face = $this->createFace($image->getId(), $person->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array($person->getId() => [$face->getId()]), + array($person->getId()+1 => [$face->getId()]) + ); + + $this->assertPersonDoNotExist($person->getId()); + $personId = $this->assertOnePerson(); + $this->assertFaces([$personId => [$face->getId()]]); + } + + /** + * Test when p1=>[f1] changes to [] + * (old person p1 should be deleted and face f1 resets its personId to null) + */ + public function testNoPersons() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person = $this->createPerson(); + $image = $this->createImage(); + $face = $this->createFace($image->getId(), $person->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array($person->getId() => [$face->getId()]), + array()); + + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $personCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, count($persons)); + $this->assertPersonDoNotExist($person->getId()); + + $this->assertFaces([null => [$face->getId()]]); + + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $faceCount); + $faces = $faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($faces)); + $this->assertNull($faces[0]->getPerson()); + $faces = $faceMapper->findFromCluster($this->user->getUID(), $person->getId(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, count($faces)); + } + + /** + * Test when p1=>[f1, f2] changes to p2=>[f1], p3=>[f2] + * (old person p1 should be deleted, and p2, p3 should be created) + */ + public function testSplitToNewPersons() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + + $person = $this->createPerson(); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person->getId()); + $face2 = $this->createFace($image->getId(), $person->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person->getId() => [$face1->getId(), $face2->getId()] + ), + array( + $person->getId()+1 => [$face1->getId()], + $person->getId()+2 => [$face2->getId()] + ) + ); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, $clusterCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertTrue(is_null($persons[0]->getName())); + $this->assertTrue(is_null($persons[1]->getName())); + $this->assertTrue($persons[0]->getIsValid()); + $this->assertTrue($persons[1]->getIsValid()); + $this->assertPersonDoNotExist($person->getId()); + + $person1Id = $persons[0]->getId(); + $person2Id = $persons[1]->getId(); + $this->assertFaces([$person1Id => [$face1->getId()], $person2Id => [$face2->getId()]]); + } + + /** + * Test when p1=>[f1, f2] changes to p1=>[f1], p2=>[f2] + * (new person p2 should be created, f2 should change person) + */ + public function testSplitToSamePerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + + $person = $this->createPerson(); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person->getId()); + $face2 = $this->createFace($image->getId(), $person->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person->getId() => [$face1->getId(), $face2->getId()] + ), + array( + $person->getId() => [$face1->getId()], + $person->getId()+1 => [$face2->getId()] + ) + ); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, $clusterCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertTrue($persons[0]->getName() === 'foo'); + $this->assertTrue($persons[1]->getName() === null); + $this->assertTrue($persons[0]->getIsValid()); + $this->assertTrue($persons[1]->getIsValid()); + $person1Id = $persons[0]->getId(); + $person2Id = $persons[1]->getId(); + $this->assertEquals($person1Id, $person->getId()); + + $this->assertFaces([$person1Id => [$face1->getId()], $person2Id => [$face2->getId()]]); + } + + /** + * Test when p1=>[f1], p2=>[f2] changes to p1=>[f1, f2], p2=>[] + * (old person p2 should be deleted, and p1 should be re-populated with both faces) + */ + public function testMergeToSamePersons() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson('foo'); + $person2 = $this->createPerson('bar'); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person2->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId()], + $person2->getId() => [$face2->getId()], + ), + array( + $person1->getId() => [$face1->getId(), $face2->getId()] + ) + ); + + $this->assertPersonDoNotExist($person2->getId()); + $personId = $this->assertOnePerson('foo'); + $this->assertEquals($personId, $person1->getId()); + $this->assertFaces([$personId => [$face1->getId(), $face2->getId()]]); + } + + /** + * Test when p1=>[f1], p2=>[f2] changes to p1=>[], p2=[], p3=>[f1, f2] + * (old persons p1 and p2 should be deleted, and p3 should be re-populated with both faces) + */ + public function testMergeToNewPersons() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson('foo'); + $person2 = $this->createPerson('bar'); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person2->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId()], + $person2->getId() => [$face2->getId()], + ), + array( + $person1->getId()+5 => [$face1->getId(), $face2->getId()] + ) + ); + + $this->assertPersonDoNotExist($person1->getId()); + $this->assertPersonDoNotExist($person2->getId()); + $personId = $this->assertOnePerson(); + $this->assertFaces([$personId => [$face1->getId(), $face2->getId()]]); + } + + /** + * Test when p1=>[f1], p2=>[f2] changes to p1=>[f2], p2=>[f1] + * (both persons and faces stay, but they change who is who) + */ + public function testSwap() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson('foo'); + $person2 = $this->createPerson('bar'); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person2->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId()], + $person2->getId() => [$face2->getId()], + ), + array( + $person1->getId() => [$face2->getId()], + $person2->getId() => [$face1->getId()], + ) + ); + + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, $personCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertTrue(strpos($persons[0]->getName(), 'foo') !== false); + $this->assertTrue(strpos($persons[1]->getName(), 'bar') !== false); + $this->assertTrue($persons[0]->getIsValid()); + $this->assertTrue($persons[1]->getIsValid()); + $this->assertEquals($person1->getId(), $persons[0]->getId()); + $this->assertEquals($person2->getId(), $persons[1]->getId()); + + $this->assertFaces([$person1->getId() => [$face2->getId()], $person2->getId() => [$face1->getId()]]); + } + + /** + * Test when p1=>[f1], p2=>[f2] changes to p1=>[f2], p3=>[f1] + * (p2 is lost and p3 is created. f2 is swapped to existing person p1) + */ + public function testHalfSwap() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson('foo'); + $person2 = $this->createPerson('bar'); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person2->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId()], + $person2->getId() => [$face2->getId()], + ), + array( + $person1->getId() => [$face2->getId()], + $person2->getId()+5 => [$face1->getId()], + ) + ); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, $clusterCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(2, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertTrue($persons[0]->getName() === 'foo'); + $this->assertTrue($persons[1]->getName() === null); + $this->assertTrue($persons[0]->getIsValid()); + $this->assertTrue($persons[1]->getIsValid()); + $this->assertEquals($person1->getId(), $persons[0]->getId()); + $person3Id = $persons[1]->getId(); + + $this->assertFaces([$person1->getId() => [$face2->getId()], $person3Id => [$face1->getId()]]); + } + + /** + * Same test as MergeClustersTest::testMergeClustersComplex + * p1=>[f1,f2,f3,f4], p2=>[f5,f6,f7,f8], p3=>[f9,f10,f11,f12], p4=>[f13,f14,f15,f16] + * is changed to + * p1=>[f1,f2,f3,f4] (same as before) + * p2=>[f5,f6,f7,f8,f17] (added f17) + * p3=>[f9,f10,f11] (removed f12) + * p4=>[f13,f14,f15] (removed f16) + * p5=>[f16,f18] (new person, moved f16 here plus new face f18) + * p6=>[f19,f20,f21] (new person, all new faces) + */ + public function testComplexToSamePerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson('foo-p1'); + $person2 = $this->createPerson('foo-p2'); + $person3 = $this->createPerson('foo-p3'); + $person4 = $this->createPerson('foo-p4'); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person1->getId()); + $face3 = $this->createFace($image->getId(), $person1->getId()); + $face4 = $this->createFace($image->getId(), $person1->getId()); + $face5 = $this->createFace($image->getId(), $person2->getId()); + $face6 = $this->createFace($image->getId(), $person2->getId()); + $face7 = $this->createFace($image->getId(), $person2->getId()); + $face8 = $this->createFace($image->getId(), $person2->getId()); + $face9 = $this->createFace($image->getId(), $person3->getId()); + $face10 = $this->createFace($image->getId(), $person3->getId()); + $face11 = $this->createFace($image->getId(), $person3->getId()); + $face12 = $this->createFace($image->getId(), $person3->getId()); + $face13 = $this->createFace($image->getId(), $person4->getId()); + $face14 = $this->createFace($image->getId(), $person4->getId()); + $face15 = $this->createFace($image->getId(), $person4->getId()); + $face16 = $this->createFace($image->getId(), $person4->getId()); + $face17 = $this->createFace($image->getId()); + $face18 = $this->createFace($image->getId()); + $face19 = $this->createFace($image->getId()); + $face20 = $this->createFace($image->getId()); + $face21 = $this->createFace($image->getId()); + $personMapper->invalidatePersons($image->getId()); + + // First person is not invalid (it will remain same, so change it back to valid) + $person1->setIsValid(true); + $personMapper->update($person1); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person2->getId() => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId()], + $person3->getId() => [$face9->getId(), $face10->getId(), $face11->getId(), $face12->getId()], + $person4->getId() => [$face13->getId(), $face14->getId(), $face15->getId(), $face16->getId()], + ), + array( + $person1->getId() => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person2->getId() => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId(), $face17->getId()], + $person3->getId() => [$face9->getId(), $face10->getId(), $face11->getId()], + $person4->getId() => [$face13->getId(), $face14->getId(), $face15->getId()], + $person1->getId() + 100 => [$face16->getId(), $face18->getId()], + $person1->getId() + 101 => [$face19->getId(), $face20->getId(), $face21->getId()], + ) + ); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(6, $clusterCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(6, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertTrue($persons[0]->getName() === 'foo-p1'); + $this->assertTrue($persons[1]->getName() === 'foo-p2'); + $this->assertTrue($persons[2]->getName() === 'foo-p3'); + $this->assertTrue($persons[3]->getName() === 'foo-p4'); + $this->assertTrue($persons[4]->getName() === null); + $this->assertTrue($persons[5]->getName() === null); + foreach ($persons as $person) { + $this->assertTrue($person->getIsValid()); + } + $this->assertEquals($person1->getId(), $persons[0]->getId()); + $this->assertEquals($person2->getId(), $persons[1]->getId()); + $this->assertEquals($person3->getId(), $persons[2]->getId()); + $this->assertEquals($person4->getId(), $persons[3]->getId()); + $person5Id = $persons[4]->getId(); + $person6Id = $persons[5]->getId(); + + $this->assertFaces([ + $person1->getId() => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person2->getId() => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId(), $face17->getId()], + $person3->getId() => [$face9->getId(), $face10->getId(), $face11->getId()], + $person4->getId() => [$face13->getId(), $face14->getId(), $face15->getId()], + $person5Id => [$face16->getId(), $face18->getId()], + $person6Id => [$face19->getId(), $face20->getId(), $face21->getId()], + null => [$face12->getId()] + ]); + } + + /** + * Same test as MergeClustersTest::testMergeClustersComplex + * and here same as testComplexToSamePerson, but with a twist that all persons are new. + * p1=>[f1,f2,f3,f4], p2=>[f5,f6,f7,f8], p3=>[f9,f10,f11,f12], p4=>[f13,f14,f15,f16] + * is changed to + * p5=>[f1,f2,f3,f4] (same as before) + * p6=>[f5,f6,f7,f8,f17] (added f17) + * p7=>[f9,f10,f11] (removed f12) + * p8=>[f13,f14,f15] (removed f16) + * p9=>[f16,f18] (new person, moved f16 here plus new face f18) + * p10=>[f19,f20,f21] (new person, all new faces) + */ + public function testComplexToNewPerson() { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + + $person1 = $this->createPerson(); + $person2 = $this->createPerson(); + $person3 = $this->createPerson(); + $person4 = $this->createPerson(); + $image = $this->createImage(); + $face1 = $this->createFace($image->getId(), $person1->getId()); + $face2 = $this->createFace($image->getId(), $person1->getId()); + $face3 = $this->createFace($image->getId(), $person1->getId()); + $face4 = $this->createFace($image->getId(), $person1->getId()); + $face5 = $this->createFace($image->getId(), $person2->getId()); + $face6 = $this->createFace($image->getId(), $person2->getId()); + $face7 = $this->createFace($image->getId(), $person2->getId()); + $face8 = $this->createFace($image->getId(), $person2->getId()); + $face9 = $this->createFace($image->getId(), $person3->getId()); + $face10 = $this->createFace($image->getId(), $person3->getId()); + $face11 = $this->createFace($image->getId(), $person3->getId()); + $face12 = $this->createFace($image->getId(), $person3->getId()); + $face13 = $this->createFace($image->getId(), $person4->getId()); + $face14 = $this->createFace($image->getId(), $person4->getId()); + $face15 = $this->createFace($image->getId(), $person4->getId()); + $face16 = $this->createFace($image->getId(), $person4->getId()); + $face17 = $this->createFace($image->getId()); + $face18 = $this->createFace($image->getId()); + $face19 = $this->createFace($image->getId()); + $face20 = $this->createFace($image->getId()); + $face21 = $this->createFace($image->getId()); + $personMapper->invalidatePersons($image->getId()); + + $personMapper->mergeClusterToDatabase($this->user->getUid(), + array( + $person1->getId() => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person2->getId() => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId()], + $person3->getId() => [$face9->getId(), $face10->getId(), $face11->getId(), $face12->getId()], + $person4->getId() => [$face13->getId(), $face14->getId(), $face15->getId(), $face16->getId()], + ), + array( + $person1->getId() + 100 => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person1->getId() + 101 => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId(), $face17->getId()], + $person1->getId() + 102 => [$face9->getId(), $face10->getId(), $face11->getId()], + $person1->getId() + 103 => [$face13->getId(), $face14->getId(), $face15->getId()], + $person1->getId() + 104 => [$face16->getId(), $face18->getId()], + $person1->getId() + 105 => [$face19->getId(), $face20->getId(), $face21->getId()], + ) + ); + + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(6, $clusterCount); + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(6, count($persons)); + usort($persons, function($p1, $p2) { + return $p1->getId() - $p2->getId(); + }); + $this->assertPersonDoNotExist($person1->getId()); + $this->assertPersonDoNotExist($person2->getId()); + $this->assertPersonDoNotExist($person3->getId()); + $this->assertPersonDoNotExist($person4->getId()); + $this->assertTrue($persons[0]->getName() === null); + $this->assertTrue($persons[1]->getName() === null); + $this->assertTrue($persons[2]->getName() === null); + $this->assertTrue($persons[3]->getName() === null); + $this->assertTrue($persons[4]->getName() === null); + $this->assertTrue($persons[5]->getName() === null); + foreach ($persons as $person) { + $this->assertTrue($person->getIsValid()); + } + $person5Id = $persons[0]->getId(); + $person6Id = $persons[1]->getId(); + $person7Id = $persons[2]->getId(); + $person8Id = $persons[3]->getId(); + $person9Id = $persons[4]->getId(); + $person10Id = $persons[5]->getId(); + + $this->assertFaces([ + $person5Id => [$face1->getId(), $face2->getId(), $face3->getId(), $face4->getId()], + $person6Id => [$face5->getId(), $face6->getId(), $face7->getId(), $face8->getId(), $face17->getId()], + $person7Id => [$face9->getId(), $face10->getId(), $face11->getId()], + $person8Id => [$face13->getId(), $face14->getId(), $face15->getId()], + $person9Id => [$face16->getId(), $face18->getId()], + $person10Id => [$face19->getId(), $face20->getId(), $face21->getId()], + null => [$face12->getId()] + ]); + } + + private function createPerson($name = 'foo'): Person { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $person = new Person(); + $person->setUser($this->user->getUID()); + $person->setName($name); + $person->setIsValid(true); + $personMapper->insert($person); + + return $person; + } + + private function createImage(): Image { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $image = new Image(); + $image->setUser($this->user->getUid()); + $image->setFile(1); + $image->setModel(ModelManager::DEFAULT_FACE_MODEL_ID); + $imageMapper->insert($image); + + return $image; + } + + private function createFace($imageId, $personId = null) { + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $face = Face::fromModel($imageId, array("left"=>0, "right"=>100, "top"=>0, "bottom"=>100, "detection_confidence"=>1.0)); + if ($personId !== null) { + $face->setPerson($personId); + } + $faceMapper->insertFace($face); + return $face; + } + + private function assertOnePerson($name = null): int { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $clusterCount = $personMapper->countClusters($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $clusterCount); + + $persons = $personMapper->findAll($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, count($persons)); + + if ($name !== null) { + // check that retains the name + $this->assertTrue($persons[0]->getName() === $name); + + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $personCount); + + // Check that it can be found using this method too + $persons = $personMapper->findByName($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID, $name); + $this->assertEquals(1, count($persons)); + } + + // Check that it can be found using this method too + $personMapper->find($this->user->getUID(), $persons[0]->getId()); + + // After clustering, person must be valid + $this->assertTrue($persons[0]->getIsValid()); + + return $persons[0]->getId(); + } + + private function assertPersonDoNotExist(int $personId) { + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + try { + $personMapper->find($this->user->getUID(), $personId); + $this->fail('Person still exist'); + } catch (DoesNotExistException $e) { + } + } + + /** + * Checks given array of faces exist in database and nothing more, and checks that faces are associated to persons. + * Keys in arrray are person IDs, and values are arrays with face IDs: + * [p1=>[f1], p2=>[f2, f3]...] + * It does as much asserts as possible by getting data from database. If key is empty, that means that face do not have person. + */ + private function assertFaces(array $personToFaces) { + $totalFaces = 0; + foreach ($personToFaces as $person=>$faces) { + $totalFaces += count($faces); + } + + // Check total faces in DB + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals($totalFaces, $faceCount); + + // Check those faces have given persons + $facesDb = $faceMapper->getFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals($totalFaces, count($facesDb)); + foreach($facesDb as $faceDb) { + foreach ($personToFaces as $person=>$faces) { + if (in_array($faceDb->getId(), $faces)) { + if ($person !== "") { + $this->assertEquals($faceDb->getPerson(), $person); + } else { + $this->assertNull($faceDb->getPerson()); + } + } + } + } + + // Check that each person have those faces (and no more) + foreach($personToFaces as $person=>$faces) { + if ($person === "") { + continue; + } + + $facesFromPerson = $faceMapper->findFromCluster($this->user->getUID(), $person, ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(count($faces), count($facesFromPerson)); + + usort($facesFromPerson, function($f1, $f2) { + return $f1->getId() - $f2->getId(); + }); + for ($i = 0; $i < count($faces); $i++) { + $this->assertEquals($faces[$i], $facesFromPerson[$i]->getId()); + } + } + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/ResetAllTest.php b/git_facerecognition/tests/Integration/ResetAllTest.php new file mode 100644 index 0000000..ccd7b02 --- /dev/null +++ b/git_facerecognition/tests/Integration/ResetAllTest.php @@ -0,0 +1,99 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\Service\FaceManagementService; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; +use OCA\FaceRecognition\Db\Face; +use OCA\FaceRecognition\Db\Image; +use OCA\FaceRecognition\Db\Person; +use OCA\FaceRecognition\Model\ModelManager; + +use Test\TestCase; + +class ResetAllTest extends IntegrationTestCase { + + /** + * Test that AddMissingImagesTask is updating app config that it finished full scan. + * Note that, in this test, we cannot check number of newly found images, + * as this instance might be in use and can lead to wrong results + */ + public function testResetAll() { + // Add one image to DB + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $image = new Image(); + $image->setUser($this->user->getUid()); + $image->setFile(1); + $image->setModel(ModelManager::DEFAULT_FACE_MODEL_ID); + $image = $imageMapper->insert($image); + $imageCount = $imageMapper->countUserImages($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $imageCount); + + // Add one person to DB + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $person = new Person(); + $person->setUser($this->user->getUID()); + $person->setIsValid(true); + $person->setName('foo'); + $person = $personMapper->insert($person); + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $personCount); // Still 0 due it has no associated faces + + // Add one face to DB + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $face = Face::fromModel($image->getId(), array("left"=>0, "right"=>100, "top"=>0, "bottom"=>100, "detection_confidence"=>1.0)); + $face->setPerson($person->getId()); + $face = $faceMapper->insertFace($face); + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $faceCount); + + // Check faces with all correct relationships + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(1, $personCount); + + // Execute reset all + $userManager = $this->container->query('OCP\IUserManager'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $faceMgmtService = new FaceManagementService($userManager, $faceMapper, $imageMapper, $personMapper, $settingsService); + $faceMgmtService->resetAllForUser($this->user->getUID()); + + // Check that everything is gone + $imageCount = $imageMapper->countUserImages($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $imageCount); + $faceCount = $faceMapper->countFaces($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $faceCount); + $personCount = $personMapper->countPersons($this->user->getUID(), ModelManager::DEFAULT_FACE_MODEL_ID); + $this->assertEquals(0, $personCount); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Integration/StaleImagesRemovalTaskTest.php b/git_facerecognition/tests/Integration/StaleImagesRemovalTaskTest.php new file mode 100644 index 0000000..acd198b --- /dev/null +++ b/git_facerecognition/tests/Integration/StaleImagesRemovalTaskTest.php @@ -0,0 +1,150 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Integration; + +use OC; +use OC\Files\View; + +use OCP\IConfig; +use OCP\IUser; +use OCP\AppFramework\App; +use OCP\AppFramework\IAppContainer; + +use OCA\FaceRecognition\Db\Image; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext; +use OCA\FaceRecognition\BackgroundJob\FaceRecognitionLogger; +use OCA\FaceRecognition\BackgroundJob\Tasks\AddMissingImagesTask; +use OCA\FaceRecognition\BackgroundJob\Tasks\StaleImagesRemovalTask; +use OCA\FaceRecognition\Model\ModelManager; +use OCA\FaceRecognition\Service\SettingsService; + +use Test\TestCase; + +class StaleImagesRemovalTaskTest extends IntegrationTestCase { + + /** + * Test that StaleImagesRemovalTask is not active, even though there should be some removals. + */ + public function testNotNeededScan() { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $image = new Image(); + $image->setUser($this->user->getUid()); + $image->setFile(1); + $image->setModel(ModelManager::DEFAULT_FACE_MODEL_ID); + $imageMapper->insert($image); + + $staleImagesRemovalTask = $this->createStaleImagesRemovalTask(); + $generator = $staleImagesRemovalTask->execute($this->context); + foreach ($generator as $_) { + } + $this->assertEquals(true, $generator->getReturn()); + + $this->assertEquals(0, $this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages']); + $imageMapper->delete($image); + } + + /** + * Test that image which exists only in database is removed when StaleImagesRemovalTask is run. + */ + public function testMissingImageRemoval() { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $image = new Image(); + $image->setUser($this->user->getUid()); + $image->setFile(2); + $image->setModel(ModelManager::DEFAULT_FACE_MODEL_ID); + $imageMapper->insert($image); + + $this->doStaleImagesRemoval(); + $this->assertEquals(1, $this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages']); + } + + /** + * Test that image under .nomedia directory is removed + */ + public function testNoMediaImageRemoval() { + // Create foo1.jpg in root and foo2.jpg in child directory + $view = new View('/' . $this->user->getUID() . '/files'); + $view->file_put_contents("foo1.jpg", "content"); + $view->mkdir('dir_nomedia'); + $view->file_put_contents("dir_nomedia/foo2.jpg", "content"); + // Create these two images in database by calling add missing images task + $this->config->setUserValue($this->user->getUID(), 'facerecognition', AddMissingImagesTask::FULL_IMAGE_SCAN_DONE_KEY, 'false'); + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + $addMissingImagesTask = new AddMissingImagesTask($imageMapper, $fileService, $settingsService); + $this->context->user = $this->user; + $generator = $addMissingImagesTask->execute($this->context); + foreach ($generator as $_) { + } + // TODO: add faces and person for those images, so we can exercise person + // invalidation and face removal when image is removed. + + // We should find 2 images now - foo1.jpg, foo2.png + $this->assertEquals(2, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + + // We should not delete anything this time + $this->doStaleImagesRemoval(); + $this->assertEquals(0, $this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages']); + + // Now add .nomedia file in subdirectory and one image (foo2.jpg) should be gone now + $view->file_put_contents("dir_nomedia/.nomedia", "content"); + $this->doStaleImagesRemoval(); + $this->assertEquals(1, $this->context->propertyBag['StaleImagesRemovalTask_staleRemovedImages']); + $this->assertEquals(1, count($imageMapper->findImagesWithoutFaces($this->user, ModelManager::DEFAULT_FACE_MODEL_ID))); + } + + /** + * Helper method to set up and do scanning + * + * @param IUser|null $contextUser Optional user to scan for. + * If not given, stale images for all users will be renived. + */ + private function doStaleImagesRemoval($contextUser = null) { + // Set config that stale image removal is needed + $this->config->setUserValue($this->user->getUID(), 'facerecognition', SettingsService::STALE_IMAGES_REMOVAL_NEEDED_KEY, 'true'); + + $staleImagesRemovalTask = $this->createStaleImagesRemovalTask(); + $this->assertNotEquals("", $staleImagesRemovalTask->description()); + + // Set user for which to do scanning, if any + $this->context->user = $contextUser; + + // Since this task returns generator, iterate until it is done + $generator = $staleImagesRemovalTask->execute($this->context); + foreach ($generator as $_) { + } + + $this->assertEquals(true, $generator->getReturn()); + } + + private function createStaleImagesRemovalTask() { + $imageMapper = $this->container->query('OCA\FaceRecognition\Db\ImageMapper'); + $faceMapper = $this->container->query('OCA\FaceRecognition\Db\FaceMapper'); + $personMapper = $this->container->query('OCA\FaceRecognition\Db\PersonMapper'); + $fileService = $this->container->query('OCA\FaceRecognition\Service\FileService'); + $settingsService = $this->container->query('OCA\FaceRecognition\Service\SettingsService'); + return new StaleImagesRemovalTask($imageMapper, $faceMapper, $personMapper, $fileService, $settingsService); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/Helper/EuclideanTest.php b/git_facerecognition/tests/Unit/Helper/EuclideanTest.php new file mode 100644 index 0000000..2bea76b --- /dev/null +++ b/git_facerecognition/tests/Unit/Helper/EuclideanTest.php @@ -0,0 +1,38 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCA\FaceRecognition\Helper\Euclidean; + +use Test\TestCase; + +class EuclideanTest extends TestCase { + + public function testEuclideans() { + $this->assertEquals(Euclidean::distance([0.0, 0.0], [0.0, 1.0]), 1.0); + $this->assertEquals(Euclidean::distance([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]), 2.0); + $this->assertEquals(Euclidean::distance([0.0, 2.5, 1.0], [0.0, 1.0, 1.0]), 1.5); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/Helper/FaceRectTest.php b/git_facerecognition/tests/Unit/Helper/FaceRectTest.php new file mode 100644 index 0000000..ef07de9 --- /dev/null +++ b/git_facerecognition/tests/Unit/Helper/FaceRectTest.php @@ -0,0 +1,59 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCA\FaceRecognition\Helper\FaceRect; + +use Test\TestCase; + +class FaceRectTest extends TestCase { + + public function testSomeOverlaps() { + $rectA = []; + $rectA['left'] = 10; + $rectA['right'] = 20; + $rectA['top'] = 10; + $rectA['bottom'] = 20; + + $rectB = []; + $rectB['left'] = 10; + $rectB['right'] = 20; + $rectB['top'] = 10; + $rectB['bottom'] = 20; + $this->assertEquals(FaceRect::overlapPercent($rectA, $rectB), 1.0); + + $rectB['left'] = 25; + $rectB['right'] = 35; + $rectB['top'] = 10; + $rectB['bottom'] = 20; + $this->assertEquals(FaceRect::overlapPercent($rectA, $rectB), 0.0); + + $rectB['left'] = 15; + $rectB['right'] = 25; + $rectB['top'] = 10; + $rectB['bottom'] = 20; + $this->assertEqualsWithDelta(FaceRect::overlapPercent($rectA, $rectB), 0.33, 0.01); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/Helper/MemoryLimitsTest.php b/git_facerecognition/tests/Unit/Helper/MemoryLimitsTest.php new file mode 100644 index 0000000..691eee3 --- /dev/null +++ b/git_facerecognition/tests/Unit/Helper/MemoryLimitsTest.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCA\FaceRecognition\Helper\MemoryLimits; + +use Test\TestCase; + +class MemoryLimitsTest extends TestCase { + + public function testReturnBytes() { + $this->assertEquals(0, MemoryLimits::returnBytes('')); + $this->assertEquals(0, MemoryLimits::returnBytes('foo')); + $this->assertEquals(0, MemoryLimits::returnBytes('foo100')); + $this->assertEquals(100, MemoryLimits::returnBytes('100')); + $this->assertEquals(100, MemoryLimits::returnBytes('100fgh')); + $this->assertEquals(101 * 1024, MemoryLimits::returnBytes('101K')); + $this->assertEquals(102 * 1024 * 1024, MemoryLimits::returnBytes('102M')); + $this->assertEquals(103 * 1024 * 1024 * 1024, MemoryLimits::returnBytes('103g')); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/Helper/RequirementsTest.php b/git_facerecognition/tests/Unit/Helper/RequirementsTest.php new file mode 100644 index 0000000..a73cc6d --- /dev/null +++ b/git_facerecognition/tests/Unit/Helper/RequirementsTest.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCA\FaceRecognition\Helper\Requirements; + +use Test\TestCase; + +class RequirementsTest extends TestCase { + + public function testReturnBool() { + $this->assertTrue(Requirements::pdlibLoaded()); + $this->assertTrue(Requirements::hasEnoughMemory()); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/Helper/TempImageTest.php b/git_facerecognition/tests/Unit/Helper/TempImageTest.php new file mode 100644 index 0000000..36a5ebe --- /dev/null +++ b/git_facerecognition/tests/Unit/Helper/TempImageTest.php @@ -0,0 +1,96 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCP\Image as OCP_Image; + +use OCA\FaceRecognition\Helper\TempImage; + +use Test\TestCase; + +class TempImageTest extends TestCase { + + private $testImage = null; + + /** + * {@inheritDoc} + */ + public function setUp(): void { + $this->testFile = \OC::$SERVERROOT . '/apps/facerecognition/tests/assets/lenna.jpg'; + } + + public function testImageTest() { + // Try an tempImage that not need change + $tempImage = new TempImage($this->testFile, + 'image/png', + 158*158, + 100); + + $this->assertFalse($tempImage->getSkipped()); + $this->assertEquals(1, $tempImage->getRatio()); + + $tempPath = $tempImage->getTempPath(); + $this->assertTrue(file_exists($tempPath)); + + $image = new OCP_Image(); + $image->loadFromFile($tempPath); + $this->assertEquals(158, imagesx($image->resource())); + $this->assertEquals(158, imagesy($image->resource())); + + $tempImage->clean(); + $this->assertFalse(file_exists($tempPath)); + + // Try image with double scaling up + $tempImage = new TempImage($this->testFile, + 'image/png', + 158*158*4, + 100); + + $this->assertFalse($tempImage->getSkipped()); + $this->assertEquals(1/2, $tempImage->getRatio()); + + $tempPath = $tempImage->getTempPath(); + $this->assertTrue(file_exists($tempPath)); + + $image = new OCP_Image(); + $image->loadFromFile($tempPath); + $this->assertEquals(158*2, imagesx($image->resource())); + $this->assertEquals(158*2, imagesy($image->resource())); + + $tempImage->clean(); + $this->assertFalse(file_exists($tempPath)); + + // Try a file smaller than the minimum + $tempImage = new TempImage($this->testFile, + 'image/png', + 640*480, + 500); + + $this->assertTrue($tempImage->getSkipped()); + $this->assertEquals(-1.0, $tempImage->getRatio()); + $this->assertEquals(158, imagesx($tempImage->resource())); + $this->assertEquals(158, imagesy($tempImage->resource())); + } + +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/LockUnlockTaskTest.php b/git_facerecognition/tests/Unit/LockUnlockTaskTest.php new file mode 100644 index 0000000..4839e0c --- /dev/null +++ b/git_facerecognition/tests/Unit/LockUnlockTaskTest.php @@ -0,0 +1,54 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use OCA\FaceRecognition\Helper\CommandLock; + +use Psr\Log\LoggerInterface; + +use Test\TestCase; + +class LockTaskTest extends TestCase { + /** + * {@inheritDoc} + */ + public function setUp(): void { + } + + public function testLockUnlock() { + $lock = CommandLock::Lock("testLockUnlock"); + $this->assertNotNull($lock); + $this->assertEquals("testLockUnlock", CommandLock::IsLockedBy()); + CommandLock::Unlock($lock); + } + + public function testDoubleLock() { + $lock = CommandLock::Lock("testDoubleLock"); + $this->assertNotNull($lock); + $lock2 = CommandLock::Lock("testDoubleLockt2"); + $this->assertNull($lock2); + $this->assertEquals("testDoubleLock", CommandLock::IsLockedBy()); + CommandLock::Unlock($lock); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/Unit/MergeClustersTest.php b/git_facerecognition/tests/Unit/MergeClustersTest.php new file mode 100644 index 0000000..3de3ff5 --- /dev/null +++ b/git_facerecognition/tests/Unit/MergeClustersTest.php @@ -0,0 +1,166 @@ + + * @copyright Copyright (c) 2018, Branko Kokanovic + * + * @author Branko Kokanovic + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\FaceRecognition\Tests\Unit; + +use Test\TestCase; + +use OCA\FaceRecognition\Db\PersonMapper; +use OCA\FaceRecognition\Db\ImageMapper; +use OCA\FaceRecognition\Db\FaceMapper; + +use OCA\FaceRecognition\Service\SettingsService; + +use OCA\FaceRecognition\BackgroundJob\Tasks\CreateClustersTask; + +class MergeClustersTest extends TestCase { + /** @var CreateClustersTask Create cluster task */ + private $createClusterTask; + + /** + * {@inheritDoc} + */ + public function setUp(): void { + $personMapper = $this->getMockBuilder(PersonMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $imageMapper = $this->getMockBuilder(ImageMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $faceMapper = $this->getMockBuilder(FaceMapper::class) + ->disableOriginalConstructor() + ->getMock(); + $settingsService = $this->getMockBuilder(SettingsService::class) + ->disableOriginalConstructor() + ->getMock(); + $this->createClusterTask = new CreateClustersTask($personMapper, $imageMapper, $faceMapper, $settingsService); + } + + /** + * Tests cluster merging. Starts with simple cases and go to more complex ones. IDs that are used + * do not have any significance, they are mostly random, except that ID<100 are for person IDs, + * and IDs>100 are reserved for face IDs (this is just convention in test, to make reading easier). + */ + public function testMergeClustersSimple() { + // Case when old cluster is empty and we get some new clusters + // + $result = $this->createClusterTask->mergeClusters(array(), array(1=>[101,102], 2=>[103,104])); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[1], [101, 102]); + $this->assertEquals($result[2], [103, 104]); + // Case when old and new cluster are completely same + // + $c = array(3=>[101,103], 4=>[105,107]); + $result = $this->createClusterTask->mergeClusters($c, $c); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[3], [101, 103]); + $this->assertEquals($result[4], [105, 107]); + // Case when cluster are the same, but person ID differ + // + $old = array(5=>[102,103], 6=>[105,106]); + $new = array(1=>[102,103], 2=>[105,106]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[5], [102, 103]); + $this->assertEquals($result[6], [105, 106]); + // Case when new faces are added to existing cluster + // + $old = array(7=>[102,103], 8=>[105,106]); + $new = array(1=>[102,103], 2=>[105,106, 107]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[7], [102, 103]); + $this->assertEquals($result[8], [105, 106, 107]); + // Case when new faces are added to new cluster + // + $old = array(3=>[110,111], 4=>[112,113]); + $new = array(1=>[110,111], 2=>[112,113], 3=>[114, 115, 116]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 3); + $this->assertEquals($result[3], [110, 111]); + $this->assertEquals($result[4], [112, 113]); + $this->assertEquals($result[5], [114, 115, 116]); + // Case when existing face "pops" to new cluster (cluster split) + // + $old = array(5=>[110,111,112], 6=>[113,114]); + $new = array(1=>[110,111], 2=>[113,114], 3=>[112]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 3); + $this->assertEquals($result[5], [110,111]); + $this->assertEquals($result[6], [113, 114]); + $this->assertEquals($result[7], [112]); + // Case when existing face is removed + // + $old = array(7=>[110,111], 8=>[113,114]); + $new = array(1=>[110], 2=>[113,114]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[7], [110]); + $this->assertEquals($result[8], [113, 114]); + // Case when all faces in cluster are removed (cluster dissapear) + // + $old = array(3=>[110,111], 4=>[113,114]); + $new = array(1=>[110,111]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 1); + $this->assertEquals($result[3], [110, 111]); + // Case when existing faces move to other cluster (cluster spil) + // + $old = array(5=>[110,111], 6=>[112,113,114]); + $new = array(1=>[110,111,112,113], 2=>[114]); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 2); + $this->assertEquals($result[5], [110, 111,112,113]); + $this->assertEquals($result[6], [114]); + } + + /** + * More complex case demostrating various use cases + */ + public function testMergeClustersComplex() { + // Case when old cluster is empty and we get some new clusters + // + $old = array( + 10=>[100,101,102,103], + 11=>[104,105,106,107], + 12=>[108,109,110,111], + 13=>[112,113,114,115] + ); + $new = array( + 1=>[100,101,102,103], // not touched + 2=>[104,105,106,107,130], // new face added to this one + 3=>[108,109,110], // one face removed + 4=>[112,113,114], // one face moved to separate cluster + 5=>[115,116], // face from cluster 4 (12) plus new face in this + 6=>[117,118,119] // completely new cluster with new faces + ); + $result = $this->createClusterTask->mergeClusters($old, $new); + $this->assertEquals(count($result), 6); + $this->assertEquals($result[10], [100, 101, 102, 103]); + $this->assertEquals($result[11], [104, 105, 106, 107, 130]); + $this->assertEquals($result[12], [108, 109, 110]); + $this->assertEquals($result[13], [112, 113, 114]); + $this->assertEquals($result[14], [115, 116]); + $this->assertEquals($result[15], [117, 118, 119]); + } +} \ No newline at end of file diff --git a/git_facerecognition/tests/assets/black.jpg b/git_facerecognition/tests/assets/black.jpg new file mode 100644 index 0000000..4cf3305 Binary files /dev/null and b/git_facerecognition/tests/assets/black.jpg differ diff --git a/git_facerecognition/tests/assets/lenna.jpg b/git_facerecognition/tests/assets/lenna.jpg new file mode 100644 index 0000000..dcc2651 Binary files /dev/null and b/git_facerecognition/tests/assets/lenna.jpg differ diff --git a/git_facerecognition/tests/bootstrap.php b/git_facerecognition/tests/bootstrap.php new file mode 100644 index 0000000..cf32df0 --- /dev/null +++ b/git_facerecognition/tests/bootstrap.php @@ -0,0 +1,11 @@ + + + + + registerEventListener + + + NodeDeletedEvent + NodeWrittenEvent + UserDeletedEvent + + + + + $tempImage + $tempImage + $tempImage + $tempImage + ?TempImage + TempImage + + + + + + + + + + + + OCP_Image + + + + + $img + $img + $img + $img + $img + $this->rootFolder + IRootFolder + IRootFolder + OCP_Image + OCP_Image + + + + + $creationTime + + + is_a($creationTime, 'DateTime') + + + + + Image + + + + + LoadSidebarListener + + + + + NodeDeletedEvent + PostDeleteListener + + + + + NodeWrittenEvent + PostWriteListener + + + + + UserDeletedEvent + UserDeletedListener + + + + + $response + $response + $response + + + + + IRootFolder + IRootFolder + + + + + + ($localPath !== false) ? $localPath : null + + + string|null + + + $this->rootFolder + $this->rootFolder + $this->rootFolder + IRootFolder + IRootFolder + StorageNotAvailableException + + + $buffer === false + $buffer === false + + + + + $this->rootFolder + IRootFolder + IRootFolder + + + + + $this->rootFolder + $this->rootFolder + IRootFolder + IRootFolder + + + + + + + + diff --git a/git_facerecognition/webpack.config.js b/git_facerecognition/webpack.config.js new file mode 100644 index 0000000..eb4bcfd --- /dev/null +++ b/git_facerecognition/webpack.config.js @@ -0,0 +1,10 @@ +const path = require('path') +const webpackConfig = require('@nextcloud/webpack-vue-config') + +webpackConfig.entry = { + 'sidebar': path.join(__dirname, 'src', 'sidebarloader.js'), + 'admin': path.join(__dirname, 'src', 'admin.js'), + 'personal': path.join(__dirname, 'src', 'personal.js'), +} + +module.exports = webpackConfig