Ver código fonte

Add nextcloud volumes

Andrea Fazzi 5 anos atrás
pai
commit
13c5b9ffd8
57 arquivos alterados com 15041 adições e 16 exclusões
  1. 14 0
      compose/karmen/config/.htaccess
  2. 0 0
      compose/karmen/config/CAN_INSTALL
  3. 4 0
      compose/karmen/config/apache-pretty-urls.config.php
  4. 4 0
      compose/karmen/config/apcu.config.php
  5. 15 0
      compose/karmen/config/apps.config.php
  6. 31 0
      compose/karmen/config/autoconfig.php
  7. 34 0
      compose/karmen/config/config.php
  8. 1719 0
      compose/karmen/config/config.sample.php
  9. 13 0
      compose/karmen/config/redis.config.php
  10. 15 0
      compose/karmen/config/smtp.config.php
  11. 16 0
      compose/karmen/data/.htaccess
  12. 0 0
      compose/karmen/data/.ocdata
  13. BIN
      compose/karmen/data/admin/files/Documents/About.odt
  14. 10 0
      compose/karmen/data/admin/files/Documents/About.txt
  15. BIN
      compose/karmen/data/admin/files/Documents/Nextcloud Flyer.pdf
  16. BIN
      compose/karmen/data/admin/files/Nextcloud Manual.pdf
  17. BIN
      compose/karmen/data/admin/files/Nextcloud intro.mp4
  18. BIN
      compose/karmen/data/admin/files/Nextcloud.png
  19. BIN
      compose/karmen/data/admin/files/Photos/Coast.jpg
  20. BIN
      compose/karmen/data/admin/files/Photos/Hummingbird.jpg
  21. BIN
      compose/karmen/data/admin/files/Photos/Nextcloud Community.jpg
  22. BIN
      compose/karmen/data/admin/files/Photos/Nut.jpg
  23. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css
  24. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css.deps
  25. BIN
      compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css.gzip
  26. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css
  27. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css.deps
  28. BIN
      compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css.gzip
  29. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/icons/icons-list.template
  30. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/icons/icons-vars.css
  31. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css
  32. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css.deps
  33. BIN
      compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css.gzip
  34. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css
  35. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css.deps
  36. BIN
      compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css.gzip
  37. 712 0
      compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js
  38. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js.deps
  39. BIN
      compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js.gzip
  40. 12430 0
      compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js
  41. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js.deps
  42. BIN
      compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js.gzip
  43. 0 0
      compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js
  44. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js.deps
  45. BIN
      compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js.gzip
  46. BIN
      compose/karmen/data/appdata_occakkp6o8zz/preview/14/256-256-crop.png
  47. BIN
      compose/karmen/data/appdata_occakkp6o8zz/preview/14/500-500-max.png
  48. 1 0
      compose/karmen/data/appdata_occakkp6o8zz/theming/0/icon-core-filetypes_text.svg
  49. 0 0
      compose/karmen/data/index.html
  50. BIN
      compose/karmen/data/nextcloud.db
  51. BIN
      compose/karmen/data/nextcloud.db-shm
  52. 0 0
      compose/karmen/data/nextcloud.db-wal
  53. 0 0
      compose/karmen/data/nextcloud.log
  54. 1 0
      compose/karmen/docker-compose.yml
  55. 4 11
      orm/document.go
  56. 7 2
      templates/documents_add_update.html.tpl
  57. 3 3
      watch.sh

+ 14 - 0
compose/karmen/config/.htaccess

@@ -0,0 +1,14 @@
+# line below if for Apache 2.4
+<ifModule mod_authz_core.c>
+Require all denied
+</ifModule>
+
+# line below if for Apache 2.2
+<ifModule !mod_authz_core.c>
+deny from all
+</ifModule>
+
+# section for Apache 2.2 and 2.4
+<ifModule mod_autoindex.c>
+IndexIgnore *
+</ifModule>

+ 0 - 0
compose/karmen/config/CAN_INSTALL


+ 4 - 0
compose/karmen/config/apache-pretty-urls.config.php

@@ -0,0 +1,4 @@
+<?php
+$CONFIG = array (
+  'htaccess.RewriteBase' => '/',
+);

+ 4 - 0
compose/karmen/config/apcu.config.php

@@ -0,0 +1,4 @@
+<?php
+$CONFIG = array (
+  'memcache.local' => '\OC\Memcache\APCu',
+);

+ 15 - 0
compose/karmen/config/apps.config.php

@@ -0,0 +1,15 @@
+<?php
+$CONFIG = array (
+  "apps_paths" => array (
+      0 => array (
+              "path"     => OC::$SERVERROOT."/apps",
+              "url"      => "/apps",
+              "writable" => false,
+      ),
+      1 => array (
+              "path"     => OC::$SERVERROOT."/custom_apps",
+              "url"      => "/custom_apps",
+              "writable" => true,
+      ),
+  ),
+);

+ 31 - 0
compose/karmen/config/autoconfig.php

@@ -0,0 +1,31 @@
+<?php
+
+$autoconfig_enabled = false;
+
+if (getenv('SQLITE_DATABASE')) {
+    $AUTOCONFIG["dbtype"] = "sqlite";
+    $AUTOCONFIG["dbname"] = getenv('SQLITE_DATABASE');
+    $autoconfig_enabled = true;
+} elseif (getenv('MYSQL_DATABASE') && getenv('MYSQL_USER') && getenv('MYSQL_PASSWORD') && getenv('MYSQL_HOST')) {
+    $AUTOCONFIG["dbtype"] = "mysql";
+    $AUTOCONFIG["dbname"] = getenv('MYSQL_DATABASE');
+    $AUTOCONFIG["dbuser"] = getenv('MYSQL_USER');
+    $AUTOCONFIG["dbpass"] = getenv('MYSQL_PASSWORD');
+    $AUTOCONFIG["dbhost"] = getenv('MYSQL_HOST');
+    $autoconfig_enabled = true;
+} elseif (getenv('POSTGRES_DB') && getenv('POSTGRES_USER') && getenv('POSTGRES_PASSWORD') && getenv('POSTGRES_HOST')) {
+    $AUTOCONFIG["dbtype"] = "pgsql";
+    $AUTOCONFIG["dbname"] = getenv('POSTGRES_DB');
+    $AUTOCONFIG["dbuser"] = getenv('POSTGRES_USER');
+    $AUTOCONFIG["dbpass"] = getenv('POSTGRES_PASSWORD');
+    $AUTOCONFIG["dbhost"] = getenv('POSTGRES_HOST');
+    $autoconfig_enabled = true;
+}
+
+if ($autoconfig_enabled) {
+    if (getenv('NEXTCLOUD_TABLE_PREFIX')) {
+        $AUTOCONFIG["dbtableprefix"] = getenv('NEXTCLOUD_TABLE_PREFIX');
+    }
+
+    $AUTOCONFIG["directory"] = getenv('NEXTCLOUD_DATA_DIR') ?: "/var/www/html/data";
+}

+ 34 - 0
compose/karmen/config/config.php

@@ -0,0 +1,34 @@
+<?php
+$CONFIG = array (
+  'htaccess.RewriteBase' => '/',
+  'memcache.local' => '\\OC\\Memcache\\APCu',
+  'apps_paths' => 
+  array (
+    0 => 
+    array (
+      'path' => '/var/www/html/apps',
+      'url' => '/apps',
+      'writable' => false,
+    ),
+    1 => 
+    array (
+      'path' => '/var/www/html/custom_apps',
+      'url' => '/custom_apps',
+      'writable' => true,
+    ),
+  ),
+  'passwordsalt' => 'LUWBpbDfP6KE5ee4vsMi/LJk/ky1ty',
+  'secret' => '1M5SVPb8cSp7aVSKQss+ha52hGSlAkq3SoGhr4gKQG7I49zF',
+  'trusted_domains' => 
+  array (
+    0 => 'localhost',
+    1 => 'nextcloud_server',
+  ),
+  'datadirectory' => '/var/www/html/data',
+  'dbtype' => 'sqlite3',
+  'version' => '17.0.0.9',
+  'overwrite.cli.url' => 'http://localhost',
+  'dbname' => 'nextcloud',
+  'installed' => true,
+  'instanceid' => 'occakkp6o8zz',
+);

+ 1719 - 0
compose/karmen/config/config.sample.php

@@ -0,0 +1,1719 @@
+<?php
+
+/**
+ * This configuration file is only provided to document the different
+ * configuration options and their usage.
+ *
+ * DO NOT COMPLETELY BASE YOUR CONFIGURATION FILE ON THIS SAMPLE. THIS MAY BREAK
+ * YOUR INSTANCE. Instead, manually copy configuration switches that you
+ * consider important for your instance to your working ``config.php``, and
+ * apply configuration options that are pertinent for your instance.
+ *
+ * This file is used to generate the configuration documentation.
+ * Please consider following requirements of the current parser:
+ *  * all comments need to start with `/**` and end with ` *\/` - each on their
+ *    own line
+ *  * add a `@see CONFIG_INDEX` to copy a previously described config option
+ *    also to this line
+ *  * everything between the ` *\/` and the next `/**` will be treated as the
+ *    config option
+ *  * use RST syntax
+ */
+
+$CONFIG = array(
+
+
+/**
+ * Default Parameters
+ *
+ * These parameters are configured by the Nextcloud installer, and are required
+ * for your Nextcloud server to operate.
+ */
+
+
+/**
+ * This is a unique identifier for your Nextcloud installation, created
+ * automatically by the installer. This example is for documentation only,
+ * and you should never use it because it will not work. A valid ``instanceid``
+ * is created when you install Nextcloud.
+ *
+ * 'instanceid' => 'd3c944a9a',
+ */
+'instanceid' => '',
+
+ /**
+  * The salt used to hash all passwords, auto-generated by the Nextcloud
+  * installer. (There are also per-user salts.) If you lose this salt you lose
+  * all your passwords. This example is for documentation only, and you should
+  * never use it.
+  *
+  * @deprecated This salt is deprecated and only used for legacy-compatibility,
+  * developers should *NOT* use this value for anything nowadays.
+  *
+  * 'passwordsalt' => 'd3c944a9af095aa08f',
+ */
+'passwordsalt' => '',
+
+/**
+ * Your list of trusted domains that users can log into. Specifying trusted
+ * domains prevents host header poisoning. Do not remove this, as it performs
+ * necessary security checks.
+ * You can specify:
+ *
+ * - the exact hostname of your host or virtual host, e.g. demo.example.org.
+ * - the exact hostname with permitted port, e.g. demo.example.org:443.
+ *   This disallows all other ports on this host
+ * - use * as a wildcard, e.g. ubos-raspberry-pi*.local will allow
+ *   ubos-raspberry-pi.local and ubos-raspberry-pi-2.local
+ */
+'trusted_domains' =>
+  array (
+    'demo.example.org',
+    'otherdomain.example.org',
+  ),
+
+
+/**
+ * Where user files are stored. The SQLite database is also stored here, when
+ * you use SQLite.
+ *
+ * Default to ``data/`` in the Nextcloud directory.
+ */
+'datadirectory' => '/var/www/nextcloud/data',
+
+/**
+ * The current version number of your Nextcloud installation. This is set up
+ * during installation and update, so you shouldn't need to change it.
+ */
+'version' => '',
+
+/**
+ * Identifies the database used with this installation. See also config option
+ * ``supportedDatabases``
+ *
+ * Available:
+ * 	- sqlite3 (SQLite3)
+ * 	- mysql (MySQL/MariaDB)
+ * 	- pgsql (PostgreSQL)
+ *
+ * Defaults to ``sqlite3``
+ */
+'dbtype' => 'sqlite3',
+
+/**
+ * Your host server name, for example ``localhost``, ``hostname``,
+ * ``hostname.example.com``, or the IP address. To specify a port use
+ * ``hostname:####``; to specify a Unix socket use
+ * ``localhost:/path/to/socket``.
+ */
+'dbhost' => '',
+
+/**
+ * The name of the Nextcloud database, which is set during installation. You
+ * should not need to change this.
+ */
+'dbname' => 'nextcloud',
+
+/**
+ * The user that Nextcloud uses to write to the database. This must be unique
+ * across Nextcloud instances using the same SQL database. This is set up during
+ * installation, so you shouldn't need to change it.
+ */
+'dbuser' => '',
+
+/**
+ * The password for the database user. This is set up during installation, so
+ * you shouldn't need to change it.
+ */
+'dbpassword' => '',
+
+/**
+ * Prefix for the Nextcloud tables in the database.
+ *
+ * Default to ``oc_``
+ */
+'dbtableprefix' => '',
+
+
+/**
+ * Indicates whether the Nextcloud instance was installed successfully; ``true``
+ * indicates a successful installation, and ``false`` indicates an unsuccessful
+ * installation.
+ *
+ * Defaults to ``false``
+ */
+'installed' => false,
+
+
+/**
+ * User Experience
+ *
+ * These optional parameters control some aspects of the user interface. Default
+ * values, where present, are shown.
+ */
+
+/**
+ * This sets the default language on your Nextcloud server, using ISO_639-1
+ * language codes such as ``en`` for English, ``de`` for German, and ``fr`` for
+ * French. It overrides automatic language detection on public pages like login
+ * or shared items. User's language preferences configured under "personal ->
+ * language" override this setting after they have logged in. Nextcloud has two
+ * distinguished language codes for German, 'de' and 'de_DE'. 'de' is used for
+ * informal German and 'de_DE' for formal German. By setting this value to 'de_DE'
+ * you can enforce the formal version of German unless the user has chosen
+ * something different explicitly.
+ *
+ * Defaults to ``en``
+ */
+'default_language' => 'en',
+
+/**
+ * With this setting a language can be forced for all users. If a language is
+ * forced, the users are also unable to change their language in the personal
+ * settings. If users shall be unable to change their language, but users have
+ * different languages, this value can be set to ``true`` instead of a language
+ * code.
+ *
+ * Defaults to ``false``
+ */
+'force_language' => 'en',
+
+/**
+ * This sets the default locale on your Nextcloud server, using ISO_639
+ * language codes such as ``en`` for English, ``de`` for German, and ``fr`` for
+ * French, and ISO-3166 country codes such as ``GB``, ``US``, ``CA``, as defined
+ * in RFC 5646. It overrides automatic locale detection on public pages like
+ * login or shared items. User's locale preferences configured under "personal
+ * -> locale" override this setting after they have logged in.
+ *
+ * Defaults to ``en``
+ */
+'default_locale' => 'en_US',
+
+/**
+ * With this setting a locale can be forced for all users. If a locale is
+ * forced, the users are also unable to change their locale in the personal
+ * settings. If users shall be unable to change their locale, but users have
+ * different languages, this value can be set to ``true`` instead of a locale
+ * code.
+ *
+ * Defaults to ``false``
+ */
+'force_locale' => 'en_US',
+
+/**
+ * Set the default app to open on login. Use the app names as they appear in the
+ * URL after clicking them in the Apps menu, such as documents, calendar, and
+ * gallery. You can use a comma-separated list of app names, so if the first
+ * app is not enabled for a user then Nextcloud will try the second one, and so
+ * on. If no enabled apps are found it defaults to the Files app.
+ *
+ * Defaults to ``files``
+ */
+'defaultapp' => 'files',
+
+/**
+ * ``true`` enables the Help menu item in the user menu (top right of the
+ * Nextcloud Web interface). ``false`` removes the Help item.
+ */
+'knowledgebaseenabled' => true,
+
+/**
+ * ``true`` allows users to change their display names (on their Personal
+ * pages), and ``false`` prevents them from changing their display names.
+ */
+'allow_user_to_change_display_name' => true,
+
+/**
+ * Lifetime of the remember login cookie. This should be larger than the
+ * session_lifetime. If it is set to 0 remember me is disabled.
+ *
+ * Defaults to ``60*60*24*15`` seconds (15 days)
+ */
+'remember_login_cookie_lifetime' => 60*60*24*15,
+
+/**
+ * The lifetime of a session after inactivity.
+ *
+ * Defaults to ``60*60*24`` seconds (24 hours)
+ */
+'session_lifetime' => 60 * 60 * 24,
+
+/**
+ * Enable or disable session keep-alive when a user is logged in to the Web UI.
+ * Enabling this sends a "heartbeat" to the server to keep it from timing out.
+ *
+ * Defaults to ``true``
+ */
+'session_keepalive' => true,
+
+/**
+ * Enforce token authentication for clients, which blocks requests using the user
+ * password for enhanced security. Users need to generate tokens in personal settings
+ * which can be used as passwords on their clients.
+ *
+ * Defaults to ``false``
+ */
+'token_auth_enforced' => false,
+
+/**
+ * Whether the bruteforce protection shipped with Nextcloud should be enabled or not.
+ *
+ * Disabling this is discouraged for security reasons.
+ *
+ * Defaults to ``true``
+ */
+'auth.bruteforce.protection.enabled' => true,
+
+/**
+ * The directory where the skeleton files are located. These files will be
+ * copied to the data directory of new users. Leave empty to not copy any
+ * skeleton files.
+ * ``{lang}`` can be used as a placeholder for the language of the user.
+ * If the directory does not exist, it falls back to non dialect (from ``de_DE``
+ * to ``de``). If that does not exist either, it falls back to ``default``
+ *
+ * Defaults to ``core/skeleton`` in the Nextcloud directory.
+ */
+'skeletondirectory' => '/path/to/nextcloud/core/skeleton',
+
+/**
+ * If your user backend does not allow password resets (e.g. when it's a
+ * read-only user backend like LDAP), you can specify a custom link, where the
+ * user is redirected to, when clicking the "reset password" link after a failed
+ * login-attempt.
+ * In case you do not want to provide any link, replace the url with 'disabled'
+ */
+'lost_password_link' => 'https://example.org/link/to/password/reset',
+
+/**
+ * Mail Parameters
+ *
+ * These configure the email settings for Nextcloud notifications and password
+ * resets.
+ */
+
+/**
+ * The return address that you want to appear on emails sent by the Nextcloud
+ * server, for example ``nc-admin@example.com``, substituting your own domain,
+ * of course.
+ */
+'mail_domain' => 'example.com',
+
+/**
+ * FROM address that overrides the built-in ``sharing-noreply`` and
+ * ``lostpassword-noreply`` FROM addresses.
+ *
+ * Defaults to different from addresses depending on the feature.
+ */
+'mail_from_address' => 'nextcloud',
+
+/**
+ * Enable SMTP class debugging.
+ *
+ * Defaults to ``false``
+ */
+'mail_smtpdebug' => false,
+
+/**
+ * Which mode to use for sending mail: ``sendmail``, ``smtp`` or ``qmail``.
+ *
+ * If you are using local or remote SMTP, set this to ``smtp``.
+ *
+ * For the ``sendmail`` option you need an installed and working email system on
+ * the server, with ``/usr/sbin/sendmail`` installed on your Unix system.
+ *
+ * For ``qmail`` the binary is /var/qmail/bin/sendmail, and it must be installed
+ * on your Unix system.
+ *
+ * Defaults to ``smtp``
+ */
+'mail_smtpmode' => 'smtp',
+
+/**
+ * This depends on ``mail_smtpmode``. Specify the IP address of your mail
+ * server host. This may contain multiple hosts separated by a semi-colon. If
+ * you need to specify the port number append it to the IP address separated by
+ * a colon, like this: ``127.0.0.1:24``.
+ *
+ * Defaults to ``127.0.0.1``
+ */
+'mail_smtphost' => '127.0.0.1',
+
+/**
+ * This depends on ``mail_smtpmode``. Specify the port for sending mail.
+ *
+ * Defaults to ``25``
+ */
+'mail_smtpport' => 25,
+
+/**
+ * This depends on ``mail_smtpmode``. This sets the SMTP server timeout, in
+ * seconds. You may need to increase this if you are running an anti-malware or
+ * spam scanner.
+ *
+ * Defaults to ``10`` seconds
+ */
+'mail_smtptimeout' => 10,
+
+/**
+ * This depends on ``mail_smtpmode``. Specify when you are using ``ssl`` or
+ * ``tls``, or leave empty for no encryption.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'mail_smtpsecure' => '',
+
+/**
+ * This depends on ``mail_smtpmode``. Change this to ``true`` if your mail
+ * server requires authentication.
+ *
+ * Defaults to ``false``
+ */
+'mail_smtpauth' => false,
+
+/**
+ * This depends on ``mail_smtpmode``. If SMTP authentication is required, choose
+ * the authentication type as ``LOGIN`` or ``PLAIN``.
+ *
+ * Defaults to ``LOGIN``
+ */
+'mail_smtpauthtype' => 'LOGIN',
+
+/**
+ * This depends on ``mail_smtpauth``. Specify the username for authenticating to
+ * the SMTP server.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'mail_smtpname' => '',
+
+/**
+ * This depends on ``mail_smtpauth``. Specify the password for authenticating to
+ * the SMTP server.
+ *
+ * Default to ``''`` (empty string)
+ */
+'mail_smtppassword' => '',
+
+/**
+ * Replaces the default mail template layout. This can be utilized if the
+ * options to modify the mail texts with the theming app is not enough.
+ * The class must extend  ``\OC\Mail\EMailTemplate``
+ */
+'mail_template_class' => '\OC\Mail\EMailTemplate',
+
+/**
+ * Email will be send by default with an HTML and a plain text body. This option
+ * allows to only send plain text emails.
+ */
+'mail_send_plaintext_only' => false,
+
+/**
+ * This depends on ``mail_smtpmode``. Array of additional streams options that
+ * will be passed to underlying Swift mailer implementation.
+ * Defaults to an empty array.
+ */
+'mail_smtpstreamoptions' => array(),
+
+/**
+ * Which mode is used for sendmail/qmail: ``smtp`` or ``pipe``.
+ *
+ * For ``smtp`` the sendmail binary is started with the parameter ``-bs``:
+ *   - Use the SMTP protocol on standard input and output.
+ *
+ * For ``pipe`` the binary is started with the parameters ``-t``:
+ *   - Read message from STDIN and extract recipients.
+ *
+ * Defaults to ``smtp``
+ */
+'mail_sendmailmode' => 'smtp',
+
+/**
+ * Proxy Configurations
+ */
+
+/**
+ * The automatic hostname detection of Nextcloud can fail in certain reverse
+ * proxy and CLI/cron situations. This option allows you to manually override
+ * the automatic detection; for example ``www.example.com``, or specify the port
+ * ``www.example.com:8080``.
+ */
+'overwritehost' => '',
+
+/**
+ * When generating URLs, Nextcloud attempts to detect whether the server is
+ * accessed via ``https`` or ``http``. However, if Nextcloud is behind a proxy
+ * and the proxy handles the ``https`` calls, Nextcloud would not know that
+ * ``ssl`` is in use, which would result in incorrect URLs being generated.
+ * Valid values are ``http`` and ``https``.
+ */
+'overwriteprotocol' => '',
+
+/**
+ * Nextcloud attempts to detect the webroot for generating URLs automatically.
+ * For example, if ``www.example.com/nextcloud`` is the URL pointing to the
+ * Nextcloud instance, the webroot is ``/nextcloud``. When proxies are in use,
+ * it may be difficult for Nextcloud to detect this parameter, resulting in
+ * invalid URLs.
+ */
+'overwritewebroot' => '',
+
+/**
+ * This option allows you to define a manual override condition as a regular
+ * expression for the remote IP address. For example, defining a range of IP
+ * addresses starting with ``10.0.0.`` and ending with 1 to 3:
+ * ``^10\.0\.0\.[1-3]$``
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'overwritecondaddr' => '',
+
+/**
+ * Use this configuration parameter to specify the base URL for any URLs which
+ * are generated within Nextcloud using any kind of command line tools (cron or
+ * occ). The value should contain the full base URL:
+ * ``https://www.example.com/nextcloud``
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'overwrite.cli.url' => '',
+
+/**
+ * To have clean URLs without `/index.php` this parameter needs to be configured.
+ *
+ * This parameter will be written as "RewriteBase" on update and installation of
+ * Nextcloud to your `.htaccess` file. While this value is often simply the URL
+ * path of the Nextcloud installation it cannot be set automatically properly in
+ * every scenario and needs thus some manual configuration.
+ *
+ * In a standard Apache setup this usually equals the folder that Nextcloud is
+ * accessible at. So if Nextcloud is accessible via "https://mycloud.org/nextcloud"
+ * the correct value would most likely be "/nextcloud". If Nextcloud is running
+ * under "https://mycloud.org/" then it would be "/".
+ *
+ * Note that the above rule is not valid in every case, as there are some rare setup
+ * cases where this may not apply. However, to avoid any update problems this
+ * configuration value is explicitly opt-in.
+ *
+ * After setting this value run `occ maintenance:update:htaccess`. Now, when the
+ * following conditions are met Nextcloud URLs won't contain `index.php`:
+ *
+ * - `mod_rewrite` is installed
+ * - `mod_env` is installed
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'htaccess.RewriteBase' => '/',
+
+/**
+ * For server setups, that don't have `mod_env` enabled or restricted (e.g. suEXEC)
+ * this parameter has to be set to true and will assume mod_rewrite.
+ *
+ * Please check, if `mod_rewrite` is active and functional before setting this
+ * parameter and you updated your .htaccess with `occ maintenance:update:htaccess`.
+ * Otherwise your nextcloud installation might not be reachable anymore.
+ * For example, try accessing resources by leaving out `index.php` in the URL.
+ */
+'htaccess.IgnoreFrontController' => false,
+
+/**
+ * The URL of your proxy server, for example ``proxy.example.com:8081``.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'proxy' => '',
+
+/**
+ * The optional authentication for the proxy to use to connect to the internet.
+ * The format is: ``username:password``.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'proxyuserpwd' => '',
+
+
+/**
+ * Deleted Items (trash bin)
+ *
+ * These parameters control the Deleted files app.
+ */
+
+/**
+ * If the trash bin app is enabled (default), this setting defines the policy
+ * for when files and folders in the trash bin will be permanently deleted.
+ * The app allows for two settings, a minimum time for trash bin retention,
+ * and a maximum time for trash bin retention.
+ * Minimum time is the number of days a file will be kept, after which it
+ * may be deleted. Maximum time is the number of days at which it is guaranteed
+ * to be deleted.
+ * Both minimum and maximum times can be set together to explicitly define
+ * file and folder deletion. For migration purposes, this setting is installed
+ * initially set to "auto", which is equivalent to the default setting in
+ * Nextcloud.
+ *
+ * Available values:
+ *
+ * * ``auto``
+ *     default setting. keeps files and folders in the trash bin for 30 days
+ *     and automatically deletes anytime after that if space is needed (note:
+ *     files may not be deleted if space is not needed).
+ * * ``D, auto``
+ *     keeps files and folders in the trash bin for D+ days, delete anytime if
+ *     space needed (note: files may not be deleted if space is not needed)
+ * * ``auto, D``
+ *     delete all files in the trash bin that are older than D days
+ *     automatically, delete other files anytime if space needed
+ * * ``D1, D2``
+ *     keep files and folders in the trash bin for at least D1 days and
+ *     delete when exceeds D2 days
+ * * ``disabled``
+ *     trash bin auto clean disabled, files and folders will be kept forever
+ *
+ * Defaults to ``auto``
+ */
+'trashbin_retention_obligation' => 'auto',
+
+
+/**
+ * File versions
+ *
+ * These parameters control the Versions app.
+ */
+
+/**
+ * If the versions app is enabled (default), this setting defines the policy
+ * for when versions will be permanently deleted.
+ * The app allows for two settings, a minimum time for version retention,
+ * and a maximum time for version retention.
+ * Minimum time is the number of days a version will be kept, after which it
+ * may be deleted. Maximum time is the number of days at which it is guaranteed
+ * to be deleted.
+ * Both minimum and maximum times can be set together to explicitly define
+ * version deletion. For migration purposes, this setting is installed
+ * initially set to "auto", which is equivalent to the default setting in
+ * Nextcloud.
+ *
+ * Available values:
+ *
+ * * ``auto``
+ *     default setting. Automatically expire versions according to expire
+ *     rules. Please refer to :doc:`../configuration_files/file_versioning` for
+ *     more information.
+ * * ``D, auto``
+ *     keep versions at least for D days, apply expire rules to all versions
+ *     that are older than D days
+ * * ``auto, D``
+ *     delete all versions that are older than D days automatically, delete
+ *     other versions according to expire rules
+ * * ``D1, D2``
+ *     keep versions for at least D1 days and delete when exceeds D2 days
+ * * ``disabled``
+ *     versions auto clean disabled, versions will be kept forever
+ *
+ * Defaults to ``auto``
+ */
+'versions_retention_obligation' => 'auto',
+
+/**
+ * Nextcloud Verifications
+ *
+ * Nextcloud performs several verification checks. There are two options,
+ * ``true`` and ``false``.
+ */
+
+/**
+ * Checks an app before install whether it uses private APIs instead of the
+ * proper public APIs. If this is set to true it will only allow to install or
+ * enable apps that pass this check.
+ *
+ * Defaults to ``false``
+ */
+'appcodechecker' => true,
+
+/**
+ * Check if Nextcloud is up-to-date and shows a notification if a new version is
+ * available.
+ *
+ * Defaults to ``true``
+ */
+'updatechecker' => true,
+
+/**
+ * URL that Nextcloud should use to look for updates
+ *
+ * Defaults to ``https://updates.nextcloud.com/updater_server/``
+ */
+'updater.server.url' => 'https://updates.nextcloud.com/updater_server/',
+
+/**
+ * The channel that Nextcloud should use to look for updates
+ *
+ * Supported values:
+ *   - ``daily``
+ *   - ``beta``
+ *   - ``stable``
+ */
+'updater.release.channel' => 'stable',
+
+/**
+ * Is Nextcloud connected to the Internet or running in a closed network?
+ *
+ * Defaults to ``true``
+ */
+'has_internet_connection' => true,
+
+/**
+ * Which domains to request to determine the availability of an Internet
+ * connection. If none of these hosts are reachable, the administration panel
+ * will show a warning. Set to an empty list to not do any such checks (warning
+ * will still be shown).
+ *
+ * Defaults to the following domains:
+ *
+ *  - www.nextcloud.com
+ *  - www.startpage.com
+ *  - www.eff.org
+ *  - www.edri.org
+ */
+'connectivity_check_domains' => array(
+	'www.nextcloud.com',
+	'www.startpage.com',
+	'www.eff.org',
+	'www.edri.org'
+),
+
+/**
+ * Allows Nextcloud to verify a working .well-known URL redirects. This is done
+ * by attempting to make a request from JS to
+ * https://your-domain.com/.well-known/caldav/
+ *
+ * Defaults to ``true``
+ */
+'check_for_working_wellknown_setup' => true,
+
+/**
+ * This is a crucial security check on Apache servers that should always be set
+ * to ``true``. This verifies that the ``.htaccess`` file is writable and works.
+ * If it is not, then any options controlled by ``.htaccess``, such as large
+ * file uploads, will not work. It also runs checks on the ``data/`` directory,
+ * which verifies that it can't be accessed directly through the Web server.
+ *
+ * Defaults to ``true``
+ */
+'check_for_working_htaccess' => true,
+
+/**
+ * In rare setups (e.g. on Openshift or docker on windows) the permissions check
+ * might block the installation while the underlying system offers no means to
+ * "correct" the permissions. In this case, set the value to false.
+ *
+ * In regular cases, if issues with permissions are encountered they should be
+ * adjusted accordingly. Changing the flag is discouraged.
+ *
+ * Defaults to ``true``
+ */
+'check_data_directory_permissions' => true,
+
+/**
+ * In certain environments it is desired to have a read-only configuration file.
+ * When this switch is set to ``true`` Nextcloud will not verify whether the
+ * configuration is writable. However, it will not be possible to configure
+ * all options via the Web interface. Furthermore, when updating Nextcloud
+ * it is required to make the configuration file writable again for the update
+ * process.
+ *
+ * Defaults to ``false``
+ */
+'config_is_read_only' => false,
+
+/**
+ * Logging
+ */
+
+/**
+ * This parameter determines where the Nextcloud logs are sent.
+ * ``file``: the logs are written to file ``nextcloud.log`` in the default
+ * Nextcloud data directory. The log file can be changed with parameter
+ * ``logfile``.
+ * ``syslog``: the logs are sent to the system log. This requires a syslog daemon
+ * to be active.
+ * ``errorlog``: the logs are sent to the PHP ``error_log`` function.
+ * ``systemd``: the logs are sent to the Systemd journal. This requires a system
+ * that runs Systemd and the Systemd journal. The PHP extension ``systemd``
+ * must be installed and active.
+ *
+ * Defaults to ``file``
+ */
+'log_type' => 'file',
+
+/**
+ * Name of the file to which the Nextcloud logs are written if parameter
+ * ``log_type`` is set to ``file``.
+ *
+ * Defaults to ``[datadirectory]/nextcloud.log``
+ */
+'logfile' => '/var/log/nextcloud.log',
+
+/**
+ * Log file mode for the Nextcloud loggin type in octal notation.
+ *
+ * Defaults to 0640 (writeable by user, readable by group).
+ */
+'logfilemode' => 0640,
+
+/**
+ * Loglevel to start logging at. Valid values are: 0 = Debug, 1 = Info, 2 =
+ * Warning, 3 = Error, and 4 = Fatal. The default value is Warning.
+ *
+ * Defaults to ``2``
+ */
+'loglevel' => 2,
+
+/**
+ * If you maintain different instances and aggregate the logs, you may want
+ * to distinguish between them. ``syslog_tag`` can be set per instance
+ * with a unique id. Only available if ``log_type`` is set to ``syslog`` or
+ * ``systemd``.
+ *
+ * The default value is ``Nextcloud``.
+ */
+'syslog_tag' => 'Nextcloud',
+
+/**
+ * Log condition for log level increase based on conditions. Once one of these
+ * conditions is met, the required log level is set to debug. This allows to
+ * debug specific requests, users or apps
+ *
+ * Supported conditions:
+ *  - ``shared_secret``: if a request parameter with the name `log_secret` is set to
+ *                this value the condition is met
+ *  - ``users``:  if the current request is done by one of the specified users,
+ *                this condition is met
+ *  - ``apps``:   if the log message is invoked by one of the specified apps,
+ *                this condition is met
+ *
+ * Defaults to an empty array.
+ */
+'log.condition' => [
+	'shared_secret' => '57b58edb6637fe3059b3595cf9c41b9',
+	'users' => ['sample-user'],
+	'apps' => ['files'],
+],
+
+/**
+ * This uses PHP.date formatting; see http://php.net/manual/en/function.date.php
+ *
+ * Defaults to ISO 8601 ``2005-08-15T15:52:01+00:00`` - see \DateTime::ATOM
+ * (https://secure.php.net/manual/en/class.datetime.php#datetime.constants.atom)
+ */
+'logdateformat' => 'F d, Y H:i:s',
+
+/**
+ * The timezone for logfiles. You may change this; see
+ * http://php.net/manual/en/timezones.php
+ *
+ * Defaults to ``UTC``
+ */
+'logtimezone' => 'Europe/Berlin',
+
+/**
+ * Append all database queries and parameters to the log file. Use this only for
+ * debugging, as your logfile will become huge.
+ */
+'log_query' => false,
+
+/**
+ * Enables log rotation and limits the total size of logfiles. The default is 0,
+ * or no rotation. Specify a size in bytes, for example 104857600 (100 megabytes
+ * = 100 * 1024 * 1024 bytes). A new logfile is created with a new name when the
+ * old logfile reaches your limit. If a rotated log file is already present, it
+ * will be overwritten.
+ *
+ * Defaults to 100 MB
+ */
+'log_rotate_size' => 100 * 1024 * 1024,
+
+
+/**
+ * Alternate Code Locations
+ *
+ * Some of the Nextcloud code may be stored in alternate locations.
+ */
+
+/**
+ * This section is for configuring the download links for Nextcloud clients, as
+ * seen in the first-run wizard and on Personal pages.
+ *
+ * Defaults to:
+ *  - Desktop client: ``https://nextcloud.com/install/#install-clients``
+ *  - Android client: ``https://play.google.com/store/apps/details?id=com.nextcloud.client``
+ *  - iOS client: ``https://itunes.apple.com/us/app/nextcloud/id1125420102?mt=8``
+ *  - iOS client app id: ``1125420102``
+ */
+'customclient_desktop' =>
+	'https://nextcloud.com/install/#install-clients',
+'customclient_android' =>
+	'https://play.google.com/store/apps/details?id=com.nextcloud.client',
+'customclient_ios' =>
+	'https://itunes.apple.com/us/app/nextcloud/id1125420102?mt=8',
+'customclient_ios_appid' =>
+		'1125420102',
+/**
+ * Apps
+ *
+ * Options for the Apps folder, Apps store, and App code checker.
+ */
+
+/**
+ * When enabled, admins may install apps from the Nextcloud app store.
+ *
+ * Defaults to ``true``
+ */
+'appstoreenabled' => true,
+
+/**
+ * Use the ``apps_paths`` parameter to set the location of the Apps directory,
+ * which should be scanned for available apps, and where user-specific apps
+ * should be installed from the Apps store. The ``path`` defines the absolute
+ * file system path to the app folder. The key ``url`` defines the HTTP Web path
+ * to that folder, starting from the Nextcloud webroot. The key ``writable``
+ * indicates if a Web server can write files to that folder.
+ */
+'apps_paths' => array(
+	array(
+		'path'=> '/var/www/nextcloud/apps',
+		'url' => '/apps',
+		'writable' => true,
+	),
+),
+
+/**
+ * @see appcodechecker
+ */
+
+/**
+ * Previews
+ *
+ * Nextcloud supports previews of image files, the covers of MP3 files, and text
+ * files. These options control enabling and disabling previews, and thumbnail
+ * size.
+ */
+
+/**
+ * By default, Nextcloud can generate previews for the following filetypes:
+ *
+ * - Image files
+ * - Covers of MP3 files
+ * - Text documents
+ *
+ * Valid values are ``true``, to enable previews, or
+ * ``false``, to disable previews
+ *
+ * Defaults to ``true``
+ */
+'enable_previews' => true,
+/**
+ * The maximum width, in pixels, of a preview. A value of ``null`` means there
+ * is no limit.
+ *
+ * Defaults to ``4096``
+ */
+'preview_max_x' => 4096,
+/**
+ * The maximum height, in pixels, of a preview. A value of ``null`` means there
+ * is no limit.
+ *
+ * Defaults to ``4096``
+ */
+'preview_max_y' => 4096,
+
+/**
+ * max file size for generating image previews with imagegd (default behavior)
+ * If the image is bigger, it'll try other preview generators, but will most
+ * likely show the default mimetype icon. Set to -1 for no limit.
+ *
+ * Defaults to ``50`` megabytes
+ */
+'preview_max_filesize_image' => 50,
+
+/**
+ * custom path for LibreOffice/OpenOffice binary
+ *
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'preview_libreoffice_path' => '/usr/bin/libreoffice',
+/**
+ * Use this if LibreOffice/OpenOffice requires additional arguments.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'preview_office_cl_parameters' =>
+	' --headless --nologo --nofirststartwizard --invisible --norestore '.
+	'--convert-to png --outdir ',
+
+/**
+ * Only register providers that have been explicitly enabled
+ *
+ * The following providers are disabled by default due to performance or privacy
+ * concerns:
+ *
+ *  - OC\Preview\Illustrator
+ *  - OC\Preview\Movie
+ *  - OC\Preview\MSOffice2003
+ *  - OC\Preview\MSOffice2007
+ *  - OC\Preview\MSOfficeDoc
+ *  - OC\Preview\OpenDocument
+ *  - OC\Preview\PDF
+ *  - OC\Preview\Photoshop
+ *  - OC\Preview\Postscript
+ *  - OC\Preview\StarOffice
+ *  - OC\Preview\SVG
+ *  - OC\Preview\TIFF
+ *  - OC\Preview\Font
+ *
+ * The following providers are not available in Microsoft Windows:
+ *
+ *  - OC\Preview\Movie
+ *  - OC\Preview\MSOfficeDoc
+ *  - OC\Preview\MSOffice2003
+ *  - OC\Preview\MSOffice2007
+ *  - OC\Preview\OpenDocument
+ *  - OC\Preview\StarOffice
+ *
+ * Defaults to the following providers:
+ *
+ *  - OC\Preview\BMP
+ *  - OC\Preview\GIF
+ *  - OC\Preview\HEIC
+ *  - OC\Preview\JPEG
+ *  - OC\Preview\MarkDown
+ *  - OC\Preview\MP3
+ *  - OC\Preview\PNG
+ *  - OC\Preview\TXT
+ *  - OC\Preview\XBitmap
+ */
+'enabledPreviewProviders' => array(
+	'OC\Preview\PNG',
+	'OC\Preview\JPEG',
+	'OC\Preview\GIF',
+	'OC\Preview\HEIC',
+	'OC\Preview\BMP',
+	'OC\Preview\XBitmap',
+	'OC\Preview\MP3',
+	'OC\Preview\TXT',
+	'OC\Preview\MarkDown'
+),
+
+/**
+ * LDAP
+ *
+ * Global settings used by LDAP User and Group Backend
+ */
+
+/**
+ * defines the interval in minutes for the background job that checks user
+ * existence and marks them as ready to be cleaned up. The number is always
+ * minutes. Setting it to 0 disables the feature.
+ * See command line (occ) methods ``ldap:show-remnants`` and ``user:delete``
+ *
+ * Defaults to ``51`` minutes
+ */
+'ldapUserCleanupInterval' => 51,
+
+/**
+ * Sort groups in the user settings by name instead of the user count
+ *
+ * By enabling this the user count beside the group name is disabled as well.
+ */
+'sort_groups_by_name' => false,
+
+/**
+ * Comments
+ *
+ * Global settings for the Comments infrastructure
+ */
+
+/**
+ * Replaces the default Comments Manager Factory. This can be utilized if an
+ * own or 3rdParty CommentsManager should be used that – for instance – uses the
+ * filesystem instead of the database to keep the comments.
+ *
+ * Defaults to ``\OC\Comments\ManagerFactory``
+ */
+'comments.managerFactory' => '\OC\Comments\ManagerFactory',
+
+/**
+ * Replaces the default System Tags Manager Factory. This can be utilized if an
+ * own or 3rdParty SystemTagsManager should be used that – for instance – uses the
+ * filesystem instead of the database to keep the tags.
+ *
+ * Defaults to ``\OC\SystemTag\ManagerFactory``
+ */
+'systemtags.managerFactory' => '\OC\SystemTag\ManagerFactory',
+
+/**
+ * Maintenance
+ *
+ * These options are for halting user activity when you are performing server
+ * maintenance.
+ */
+
+/**
+ * Enable maintenance mode to disable Nextcloud
+ *
+ * If you want to prevent users from logging in to Nextcloud before you start
+ * doing some maintenance work, you need to set the value of the maintenance
+ * parameter to true. Please keep in mind that users who are already logged-in
+ * are kicked out of Nextcloud instantly.
+ *
+ * Defaults to ``false``
+ */
+'maintenance' => false,
+
+
+/**
+ * SSL
+ */
+
+/**
+ * Extra SSL options to be used for configuration.
+  *
+ * Defaults to an empty array.
+ */
+'openssl' => array(
+	'config' => '/absolute/location/of/openssl.cnf',
+),
+
+/**
+ * Memory caching backend configuration
+ *
+ * Available cache backends:
+ *
+ * * ``\OC\Memcache\APCu``       APC user backend
+ * * ``\OC\Memcache\ArrayCache`` In-memory array-based backend (not recommended)
+ * * ``\OC\Memcache\Memcached``  Memcached backend
+ * * ``\OC\Memcache\Redis``      Redis backend
+ *
+ * Advice on choosing between the various backends:
+ *
+ * * APCu should be easiest to install. Almost all distributions have packages.
+ *   Use this for single user environment for all caches.
+ * * Use Redis or Memcached for distributed environments.
+ *   For the local cache (you can configure two) take APCu.
+ */
+
+/**
+ * Memory caching backend for locally stored data
+ *
+ * * Used for host-specific data, e.g. file paths
+ *
+ * Defaults to ``none``
+ */
+'memcache.local' => '\OC\Memcache\APCu',
+
+/**
+ * Memory caching backend for distributed data
+ *
+ * * Used for installation-specific data, e.g. database caching
+ * * If unset, defaults to the value of memcache.local
+ *
+ * Defaults to ``none``
+ */
+'memcache.distributed' => '\OC\Memcache\Memcached',
+
+/**
+ * Connection details for redis to use for memory caching in a single server configuration.
+ *
+ * For enhanced security it is recommended to configure Redis
+ * to require a password. See http://redis.io/topics/security
+ * for more information.
+ */
+'redis' => [
+	'host' => 'localhost', // can also be a unix domain socket: '/tmp/redis.sock'
+	'port' => 6379,
+	'timeout' => 0.0,
+	'password' => '', // Optional, if not defined no password will be used.
+	'dbindex' => 0, // Optional, if undefined SELECT will not run and will use Redis Server's default DB Index.
+],
+
+/**
+ * Connection details for a Redis Cluster
+ *
+ * Only for use with Redis Clustering, for Sentinel-based setups use the single
+ * server configuration above, and perform HA on the hostname.
+ *
+ * Redis Cluster support requires the php module phpredis in version 3.0.0 or
+ * higher.
+ *
+ * Available failover modes:
+ *  - \RedisCluster::FAILOVER_NONE - only send commands to master nodes (default)
+ *  - \RedisCluster::FAILOVER_ERROR - failover to slaves for read commands if master is unavailable (recommended)
+ *  - \RedisCluster::FAILOVER_DISTRIBUTE - randomly distribute read commands across master and slaves
+ *
+ * WARNING: FAILOVER_DISTRIBUTE is a not recommended setting and we strongly
+ * suggest to not use it if you use Redis for file locking. Due to the way Redis
+ * is synchronized it could happen, that the read for an existing lock is
+ * scheduled to a slave that is not fully synchronized with the connected master
+ * which then causes a FileLocked exception.
+ *
+ * See https://redis.io/topics/cluster-spec for details about the Redis cluster
+ *
+ * Authentication works with phpredis version 4.2.1+. See
+ * https://github.com/phpredis/phpredis/commit/c5994f2a42b8a348af92d3acb4edff1328ad8ce1
+ */
+'redis.cluster' => [
+	'seeds' => [ // provide some/all of the cluster servers to bootstrap discovery, port required
+		'localhost:7000',
+		'localhost:7001',
+	],
+	'timeout' => 0.0,
+	'read_timeout' => 0.0,
+	'failover_mode' => \RedisCluster::FAILOVER_ERROR,
+	'password' => '', // Optional, if not defined no password will be used.
+],
+
+
+/**
+ * Server details for one or more memcached servers to use for memory caching.
+ */
+'memcached_servers' => array(
+	// hostname, port and optional weight. Also see:
+	// http://www.php.net/manual/en/memcached.addservers.php
+	// http://www.php.net/manual/en/memcached.addserver.php
+	array('localhost', 11211),
+	//array('other.host.local', 11211),
+),
+
+/**
+ * Connection options for memcached, see http://apprize.info/php/scaling/15.html
+ */
+'memcached_options' => array(
+	// Set timeouts to 50ms
+	\Memcached::OPT_CONNECT_TIMEOUT => 50,
+	\Memcached::OPT_RETRY_TIMEOUT =>   50,
+	\Memcached::OPT_SEND_TIMEOUT =>    50,
+	\Memcached::OPT_RECV_TIMEOUT =>    50,
+	\Memcached::OPT_POLL_TIMEOUT =>    50,
+
+	// Enable compression
+	\Memcached::OPT_COMPRESSION =>          true,
+
+	// Turn on consistent hashing
+	\Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
+
+	// Enable Binary Protocol
+	\Memcached::OPT_BINARY_PROTOCOL =>      true,
+
+	// Binary serializer vill be enabled if the igbinary PECL module is available
+	//\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_IGBINARY,
+),
+
+
+/**
+ * Location of the cache folder, defaults to ``data/$user/cache`` where
+ * ``$user`` is the current user. When specified, the format will change to
+ * ``$cache_path/$user`` where ``$cache_path`` is the configured cache directory
+ * and ``$user`` is the user.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'cache_path' => '',
+
+/**
+ * TTL of chunks located in the cache folder before they're removed by
+ * garbage collection (in seconds). Increase this value if users have
+ * issues uploading very large files via the Nextcloud Client as upload isn't
+ * completed within one day.
+ *
+ * Defaults to ``60*60*24`` (1 day)
+ */
+'cache_chunk_gc_ttl' => 60*60*24,
+
+/**
+ * Using Object Store with Nextcloud
+ */
+
+/**
+ * This example shows how to configure Nextcloud to store all files in a
+ * swift object storage.
+ *
+ * It is important to note that Nextcloud in object store mode will expect
+ * exclusive access to the object store container because it only stores the
+ * binary data for each file. The metadata is currently kept in the local
+ * database for performance reasons.
+ *
+ * WARNING: The current implementation is incompatible with any app that uses
+ * direct file IO and circumvents our virtual filesystem. That includes
+ * Encryption and Gallery. Gallery will store thumbnails directly in the
+ * filesystem and encryption will cause severe overhead because key files need
+ * to be fetched in addition to any requested file.
+ *
+ * One way to test is applying for a trystack account at http://trystack.org/
+ */
+'objectstore' => [
+	'class' => 'OC\\Files\\ObjectStore\\Swift',
+	'arguments' => [
+		// trystack will use your facebook id as the user name
+		'username' => 'facebook100000123456789',
+		// in the trystack dashboard go to user -> settings -> API Password to
+		// generate a password
+		'password' => 'Secr3tPaSSWoRdt7',
+		// must already exist in the objectstore, name can be different
+		'container' => 'nextcloud',
+		// prefix to prepend to the fileid, default is 'oid:urn:'
+		'objectPrefix' => 'oid:urn:',
+		// create the container if it does not exist. default is false
+		'autocreate' => true,
+		// required, dev-/trystack defaults to 'RegionOne'
+		'region' => 'RegionOne',
+		// The Identity / Keystone endpoint
+		'url' => 'http://8.21.28.222:5000/v2.0',
+		// required on dev-/trystack
+		'tenantName' => 'facebook100000123456789',
+		// dev-/trystack uses swift by default, the lib defaults to 'cloudFiles'
+		// if omitted
+		'serviceName' => 'swift',
+		// The Interface / url Type, optional
+		'urlType' => 'internal'
+	],
+],
+
+/**
+ * To use swift V3
+ */
+'objectstore' => [
+	'class' => 'OC\\Files\\ObjectStore\\Swift',
+	'arguments' => [
+		'autocreate' => true,
+		'user' => [
+			'name' => 'swift',
+			'password' => 'swift',
+			'domain' => [
+				'name' => 'default',
+			],
+		],
+		'scope' => [
+			'project' => [
+				'name' => 'service',
+				'domain' => [
+					'name' => 'default',
+				],
+			],
+		],
+		'tenantName' => 'service',
+		'serviceName' => 'swift',
+		'region' => 'regionOne',
+		'url' => 'http://yourswifthost:5000/v3',
+		'bucket' => 'nextcloud',
+	],
+],
+
+
+/**
+ * Sharing
+ *
+ * Global settings for Sharing
+ */
+
+/**
+ * Replaces the default Share Provider Factory. This can be utilized if
+ * own or 3rdParty Share Providers are used that – for instance – use the
+ * filesystem instead of the database to keep the share information.
+ *
+ * Defaults to ``\OC\Share20\ProviderFactory``
+ */
+'sharing.managerFactory' => '\OC\Share20\ProviderFactory',
+
+/**
+ * Define max number of results returned by the user search for auto-completion
+ * Default is unlimited (value set to 0).
+ */
+'sharing.maxAutocompleteResults' => 0,
+
+/**
+ * Define the minimum length of the search string before we start auto-completion
+ * Default is no limit (value set to 0)
+ */
+'sharing.minSearchStringLength' => 0,
+
+/**
+ * All other configuration options
+ */
+
+/**
+ * Additional driver options for the database connection, eg. to enable SSL
+ * encryption in MySQL or specify a custom wait timeout on a cheap hoster.
+ */
+'dbdriveroptions' => array(
+	PDO::MYSQL_ATTR_SSL_CA => '/file/path/to/ca_cert.pem',
+	PDO::MYSQL_ATTR_INIT_COMMAND => 'SET wait_timeout = 28800'
+),
+
+/**
+ * sqlite3 journal mode can be specified using this configuration parameter -
+ * can be 'WAL' or 'DELETE' see for more details https://www.sqlite.org/wal.html
+ */
+'sqlite.journal_mode' => 'DELETE',
+
+/**
+ * During setup, if requirements are met (see below), this setting is set to true
+ * and MySQL can handle 4 byte characters instead of 3 byte characters.
+ *
+ * If you want to convert an existing 3-byte setup into a 4-byte setup please
+ * set the parameters in MySQL as mentioned below and run the migration command:
+ * ./occ db:convert-mysql-charset
+ * The config setting will be set automatically after a successful run.
+ *
+ * Consult the documentation for more details.
+ *
+ * MySQL requires a special setup for longer indexes (> 767 bytes) which are
+ * needed:
+ *
+ * [mysqld]
+ * innodb_large_prefix=ON
+ * innodb_file_format=Barracuda
+ * innodb_file_per_table=ON
+ *
+ * Tables will be created with
+ *  * character set: utf8mb4
+ *  * collation:     utf8mb4_bin
+ *  * row_format:    compressed
+ *
+ * See:
+ * https://dev.mysql.com/doc/refman/5.7/en/charset-unicode-utf8mb4.html
+ * https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix
+ * https://mariadb.com/kb/en/mariadb/xtradbinnodb-server-system-variables/#innodb_large_prefix
+ * http://www.tocker.ca/2013/10/31/benchmarking-innodb-page-compression-performance.html
+ * http://mechanics.flite.com/blog/2014/07/29/using-innodb-large-prefix-to-avoid-error-1071/
+ */
+'mysql.utf8mb4' => false,
+
+/**
+ * Database types that are supported for installation.
+ *
+ * Available:
+ * 	- sqlite (SQLite3)
+ * 	- mysql (MySQL)
+ * 	- pgsql (PostgreSQL)
+ * 	- oci (Oracle)
+ *
+ * Defaults to the following databases:
+ *  - sqlite (SQLite3)
+ *  - mysql (MySQL)
+ *  - pgsql (PostgreSQL)
+ */
+'supportedDatabases' => array(
+	'sqlite',
+	'mysql',
+	'pgsql',
+	'oci',
+),
+
+/**
+ * Override where Nextcloud stores temporary files. Useful in situations where
+ * the system temporary directory is on a limited space ramdisk or is otherwise
+ * restricted, or if external storages which do not support streaming are in
+ * use.
+ *
+ * The Web server user must have write access to this directory.
+ */
+'tempdirectory' => '/tmp/nextcloudtemp',
+
+/**
+ * The hashing cost used by hashes generated by Nextcloud
+ * Using a higher value requires more time and CPU power to calculate the hashes
+ */
+'hashingCost' => 10,
+
+/**
+ * Blacklist a specific file or files and disallow the upload of files
+ * with this name. ``.htaccess`` is blocked by default.
+ * WARNING: USE THIS ONLY IF YOU KNOW WHAT YOU ARE DOING.
+ *
+ * Defaults to ``array('.htaccess')``
+ */
+'blacklisted_files' => array('.htaccess'),
+
+/**
+ * Define a default folder for shared files and folders other than root.
+ * Changes to this value will only have effect on new shares.
+ *
+ * Defaults to ``/``
+ */
+'share_folder' => '/',
+
+/**
+ * If you are applying a theme to Nextcloud, enter the name of the theme here.
+ * The default location for themes is ``nextcloud/themes/``.
+ *
+ * Defaults to the theming app which is shipped since Nextcloud 9
+ */
+'theme' => '',
+
+/**
+ * The default cipher for encrypting files. Currently AES-128-CFB and
+ * AES-256-CFB are supported.
+ */
+'cipher' => 'AES-256-CFB',
+
+/**
+ * The minimum Nextcloud desktop client version that will be allowed to sync with
+ * this server instance. All connections made from earlier clients will be denied
+ * by the server. Defaults to the minimum officially supported Nextcloud desktop
+ * clientversion at the time of release of this server version.
+ *
+ * When changing this, note that older unsupported versions of the Nextcloud desktop
+ * client may not function as expected, and could lead to permanent data loss for
+ * clients or other unexpected results.
+ *
+ * Defaults to ``2.0.0``
+ */
+'minimum.supported.desktop.version' => '2.0.0',
+
+/**
+ * EXPERIMENTAL: option whether to include external storage in quota
+ * calculation, defaults to false.
+ *
+ * Defaults to ``false``
+ */
+'quota_include_external_storage' => false,
+
+/**
+ * When an external storage is unavailable for some reasons, it will be flagged
+ * as such for 10 minutes. When the trigger is a failed authentication attempt
+ * the delay is higher and can be controlled with this option. The motivation
+ * is to make account lock outs at Active Directories (and compatible) more
+ * unlikely.
+ *
+ * Defaults to ``1800`` (seconds)
+ */
+'external_storage.auth_availability_delay' => 1800,
+
+/**
+ * Specifies how often the local filesystem (the Nextcloud data/ directory, and
+ * NFS mounts in data/) is checked for changes made outside Nextcloud. This
+ * does not apply to external storages.
+ *
+ * 0 -> Never check the filesystem for outside changes, provides a performance
+ * increase when it's certain that no changes are made directly to the
+ * filesystem
+ *
+ * 1 -> Check each file or folder at most once per request, recommended for
+ * general use if outside changes might happen.
+ *
+ * Defaults to ``0``
+ */
+'filesystem_check_changes' => 0,
+
+/**
+ * By default Nextcloud will store the part files created during upload in the
+ * same storage as the upload target. Setting this to false will store the part
+ * files in the root of the users folder which might be required to work with certain
+ * external storage setups that have limited rename capabilities.
+ *
+ * Defaults to ``true``
+ */
+'part_file_in_storage' => true,
+
+/**
+ * Where ``mount.json`` file should be stored, defaults to ``data/mount.json``
+ * in the Nextcloud directory.
+ *
+ * Defaults to ``data/mount.json`` in the Nextcloud directory.
+ */
+'mount_file' => '/var/www/nextcloud/data/mount.json',
+
+/**
+ * When ``true``, prevent Nextcloud from changing the cache due to changes in
+ * the filesystem for all storage.
+ *
+ * Defaults to ``false``
+ */
+'filesystem_cache_readonly' => false,
+
+/**
+ * Secret used by Nextcloud for various purposes, e.g. to encrypt data. If you
+ * lose this string there will be data corruption.
+ */
+'secret' => '',
+
+/**
+ * List of trusted proxy servers
+ *
+ * You may set this to an array containing a combination of
+ * - IPv4 addresses, e.g. `192.168.2.123`
+ * - IPv4 ranges in CIDR notation, e.g. `192.168.2.0/24`
+ * - IPv6 addresses, e.g. `fd9e:21a7:a92c:2323::1`
+ *
+ * _(CIDR notation for IPv6 is currently work in progress and thus not
+ * available as of yet)_
+ *
+ * When an incoming request's `REMOTE_ADDR` matches any of the IP addresses
+ * specified here, it is assumed to be a proxy instead of a client. Thus, the
+ * client IP will be read from the HTTP header specified in
+ * `forwarded_for_headers` instead of from `REMOTE_ADDR`.
+ *
+ * So if you configure `trusted_proxies`, also consider setting
+ * `forwarded_for_headers` which otherwise defaults to `HTTP_X_FORWARDED_FOR`
+ * (the `X-Forwarded-For` header).
+ *
+ * Defaults to an empty array.
+ */
+'trusted_proxies' => array('203.0.113.45', '198.51.100.128', '192.168.2.0/24'),
+
+/**
+ * Headers that should be trusted as client IP address in combination with
+ * `trusted_proxies`. If the HTTP header looks like 'X-Forwarded-For', then use
+ * 'HTTP_X_FORWARDED_FOR' here.
+ *
+ * If set incorrectly, a client can spoof their IP address as visible to
+ * Nextcloud, bypassing access controls and making logs useless!
+ *
+ * Defaults to ``'HTTP_X_FORWARDED_FOR'``
+ */
+'forwarded_for_headers' => array('HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR'),
+
+/**
+ * max file size for animating gifs on public-sharing-site.
+ * If the gif is bigger, it'll show a static preview
+ *
+ * Value represents the maximum filesize in megabytes. Set to ``-1`` for
+ * no limit.
+ *
+ * Defaults to ``10`` megabytes
+ */
+'max_filesize_animated_gifs_public_sharing' => 10,
+
+
+/**
+ * Enables transactional file locking.
+ * This is enabled by default.
+ *
+ * Prevents concurrent processes from accessing the same files
+ * at the same time. Can help prevent side effects that would
+ * be caused by concurrent operations. Mainly relevant for
+ * very large installations with many users working with
+ * shared files.
+ *
+ * Defaults to ``true``
+ */
+'filelocking.enabled' => true,
+
+/**
+ * Set the lock's time-to-live in seconds.
+ *
+ * Any lock older than this will be automatically cleaned up.
+ *
+ * Defaults to ``60*60`` seconds (1 hour) or the php
+ *             max_execution_time, whichever is higher.
+ */
+'filelocking.ttl' => 60*60,
+
+/**
+ * Memory caching backend for file locking
+ *
+ * Because most memcache backends can clean values without warning using redis
+ * is highly recommended to *avoid data loss*.
+ *
+ * Defaults to ``none``
+ */
+'memcache.locking' => '\\OC\\Memcache\\Redis',
+
+/**
+ * Enable locking debug logging
+ *
+ * Note that this can lead to a very large volume of log items being written which can lead
+ * to performance degradation and large log files on busy instance.
+ *
+ * Thus enabling this in production for longer periods of time is not recommended
+ * or should be used together with the ``log.condition`` setting.
+ */
+'filelocking.debug' => false,
+
+/**
+ * Disable the web based updater
+ */
+'upgrade.disable-web' => false,
+
+/**
+ * Set this Nextcloud instance to debugging mode
+ *
+ * Only enable this for local development and not in production environments
+ * This will disable the minifier and outputs some additional debug information
+ *
+ * Defaults to ``false``
+ */
+'debug' => false,
+
+/**
+ * Sets the data-fingerprint of the current data served
+ *
+ * This is a property used by the clients to find out if a backup has been
+ * restored on the server. Once a backup is restored run
+ * ./occ maintenance:data-fingerprint
+ * To set this to a new value.
+ *
+ * Updating/Deleting this value can make connected clients stall until
+ * the user has resolved conflicts.
+ *
+ * Defaults to ``''`` (empty string)
+ */
+'data-fingerprint' => '',
+
+/**
+ * This entry is just here to show a warning in case somebody copied the sample
+ * configuration. DO NOT ADD THIS SWITCH TO YOUR CONFIGURATION!
+ *
+ * If you, brave person, have read until here be aware that you should not
+ * modify *ANY* settings in this file without reading the documentation.
+ */
+'copied_sample_config' => true,
+
+/**
+ * use a custom lookup server to publish user data
+ */
+'lookup_server' => 'https://lookup.nextcloud.com',
+
+/**
+ * set to true if the server is used in a setup based on Nextcloud's Global Scale architecture
+ */
+'gs.enabled' => false,
+
+/**
+ * by default federation is only used internally in a Global Scale setup
+ * If you want to allow federation outside of your environment set it to 'global'
+ */
+'gs.federation' => 'internal',
+
+/**
+ * List of incompatible user agents opted out from Same Site Cookie Protection.
+ * Some user agents are notorious and don't really properly follow HTTP
+ * specifications. For those, have an opt-out.
+ *
+ * WARNING: only use this if you know what you are doing
+ */
+'csrf.optout' => array(
+	'/^WebDAVFS/', // OS X Finder
+	'/^Microsoft-WebDAV-MiniRedir/', // Windows webdav drive
+),
+
+/**
+ * By default there is on public pages a link shown that allows users to
+ * learn about the "simple sign up" - see https://nextcloud.com/signup/
+ *
+ * If this is set to "false" it will not show the link.
+ */
+'simpleSignUpLink.shown' => true,
+
+/**
+ * By default autocompletion is enabled for the login form on Nextcloud's login page.
+ * While this is enabled, browsers are allowed to "remember" login names and such.
+ * Some companies require it to be disabled to comply with their security policy.
+ *
+ * Simply set this property to "false", if you want to turn this feature off.
+ */
+
+'login_form_autocomplete' => true,
+);

+ 13 - 0
compose/karmen/config/redis.config.php

@@ -0,0 +1,13 @@
+<?php
+if (getenv('REDIS_HOST')) {
+  $CONFIG = array (
+    'memcache.distributed' => '\OC\Memcache\Redis',
+    'memcache.locking' => '\OC\Memcache\Redis',
+    'redis' => array(
+      'host' => getenv('REDIS_HOST'),
+      'port' => getenv('REDIS_HOST_PORT') ?: 6379,
+      'password' => getenv('REDIS_HOST_PASSWORD'),
+    ),
+  );
+}
+

+ 15 - 0
compose/karmen/config/smtp.config.php

@@ -0,0 +1,15 @@
+<?php
+if (getenv('SMTP_HOST') && getenv('MAIL_FROM_ADDRESS') && getenv('MAIL_DOMAIN')) {
+  $CONFIG = array (
+    'mail_smtpmode' => 'smtp',
+    'mail_smtphost' => getenv('SMTP_HOST'),
+    'mail_smtpport' => getenv('SMTP_PORT') ?: (getenv('SMTP_SECURE') ? 465 : 25),
+    'mail_smtpsecure' => getenv('SMTP_SECURE') ?: '',
+    'mail_smtpauth' => getenv('SMTP_NAME') && getenv('SMTP_PASSWORD'),
+    'mail_smtpauthtype' => getenv('SMTP_AUTHTYPE') ?: 'LOGIN',
+    'mail_smtpname' => getenv('SMTP_NAME') ?: '',
+    'mail_smtppassword' => getenv('SMTP_PASSWORD') ?: '',
+    'mail_from_address' => getenv('MAIL_FROM_ADDRESS'),
+    'mail_domain' => getenv('MAIL_DOMAIN'),
+  );
+}

+ 16 - 0
compose/karmen/data/.htaccess

@@ -0,0 +1,16 @@
+# Generated by Nextcloud on 2019-10-04 08:27:18
+# line below if for Apache 2.4
+<ifModule mod_authz_core.c>
+Require all denied
+</ifModule>
+
+# line below if for Apache 2.2
+<ifModule !mod_authz_core.c>
+deny from all
+Satisfy All
+</ifModule>
+
+# section for Apache 2.2 and 2.4
+<ifModule mod_autoindex.c>
+IndexIgnore *
+</ifModule>

+ 0 - 0
compose/karmen/data/.ocdata


BIN
compose/karmen/data/admin/files/Documents/About.odt


+ 10 - 0
compose/karmen/data/admin/files/Documents/About.txt

@@ -0,0 +1,10 @@
+About Nextcloud
+
+Welcome to Nextcloud, your self-hosted file sync and share solution.
+Nextcloud is the open source file sync and share software for everyone from individuals to large enterprises and service providers. Nextcloud provides a safe, secure and compliant file sync and share solution on servers you control.
+With Nextcloud you can share one or more folders on your PC, and sync them with your Nextcloud server. Place files in your local shared directories, and those files are immediately synced to the server, and then to other PCs via the desktop client. Not near a desktop client? No problem, simply log in with the web client and manage your files there. The Android and iOS mobile apps allow you to browse, download and upload photos and videos.
+Whether using a mobile device, a workstation, or a web client, Nextcloud provides the ability to put the right files in the right hands at the right time on any device in one simple-to-use, secure, private and controlled solution.
+
+
+All example pictures & music are licensed under Creative Commons Attribution.
+

BIN
compose/karmen/data/admin/files/Documents/Nextcloud Flyer.pdf


BIN
compose/karmen/data/admin/files/Nextcloud Manual.pdf


BIN
compose/karmen/data/admin/files/Nextcloud intro.mp4


BIN
compose/karmen/data/admin/files/Nextcloud.png


BIN
compose/karmen/data/admin/files/Photos/Coast.jpg


BIN
compose/karmen/data/admin/files/Photos/Hummingbird.jpg


BIN
compose/karmen/data/admin/files/Photos/Nextcloud Community.jpg


BIN
compose/karmen/data/admin/files/Photos/Nut.jpg


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/core\/css\/variables.scss":1570177633,"\/var\/www\/html\/core\/css\/functions.scss":1570177633,"\/var\/www\/html\/apps\/activity\/css\/style.scss":1570177632}

BIN
compose/karmen/data/appdata_occakkp6o8zz/css/activity/e2ca-37d0-style.css.gzip


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/core\/css\/variables.scss":1570177633,"\/var\/www\/html\/core\/css\/functions.scss":1570177633,"\/var\/www\/html\/apps\/files\/css\/merged.scss":1570177632,"\/var\/www\/html\/apps\/files\/css\/files.scss":1570177632,"\/var\/www\/html\/apps\/files\/css\/upload.scss":1570177632,"\/var\/www\/html\/apps\/files\/css\/mobile.scss":1570177632,"\/var\/www\/html\/apps\/files\/css\/detailsView.scss":1570177632,"\/var\/www\/html\/core\/css\/whatsnew.scss":1570177633}

BIN
compose/karmen/data/appdata_occakkp6o8zz/css/files/4039-37d0-merged.css.gzip


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/icons/icons-list.template


Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/icons/icons-vars.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/core\/css\/variables.scss":1570177633,"\/var\/www\/html\/core\/css\/functions.scss":1570177633,"\/var\/www\/html\/apps\/notifications\/css\/styles.scss":1570177632}

BIN
compose/karmen/data/appdata_occakkp6o8zz/css/notifications/daf8-37d0-styles.css.gzip


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/core\/css\/variables.scss":1570177633,"\/var\/www\/html\/core\/css\/functions.scss":1570177633,"\/var\/www\/html\/apps\/text\/css\/icons.scss":1570177632}

BIN
compose/karmen/data/appdata_occakkp6o8zz/css/text/7d23-37d0-icons.css.gzip


+ 712 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js

@@ -0,0 +1,712 @@
+/**
+ * @copyright (c) 2016 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ */
+
+(function(OC, OCA) {
+	OCA.Activity = OCA.Activity || {};
+
+	OCA.Activity.RichObjectStringParser = {
+		avatarsEnabled: true,
+
+		/**
+		 * @param {string} message
+		 * @param {Object} parameters
+		 * @returns {string}
+		 */
+		parseMessage: function(message, parameters) {
+			message = escapeHTML(message);
+			var self = this,
+				regex = /\{([a-z\-_0-9]+)\}/gi,
+				matches = message.match(regex);
+
+			_.each(matches, function(parameter) {
+				parameter = parameter.substring(1, parameter.length - 1);
+				if (!parameters.hasOwnProperty(parameter) || !parameters[parameter]) {
+					// Malformed translation?
+					console.error('Potential malformed ROS string: parameter {' + parameter + '} was found in the string but is missing from the parameter list');
+					return;
+				}
+
+				var parsed = self.parseParameter(parameters[parameter]);
+				message = message.replace('{' + parameter + '}', parsed);
+			});
+
+			return message.replace(new RegExp("\n", 'g'), '<br>');
+		},
+
+		/**
+		 * @param {Object} parameter
+		 * @param {string} parameter.type
+		 * @param {string} parameter.id
+		 * @param {string} parameter.name
+		 * @param {string} parameter.link
+		 */
+		parseParameter: function(parameter) {
+			switch (parameter.type) {
+				case 'file':
+					return this.parseFileParameter(parameter).trim("\n");
+
+				case 'systemtag':
+					var name = parameter.name;
+					if (parameter.visibility !== '1') {
+						name = t('activity', '{name} (invisible)', parameter);
+					} else if (parameter.assignable !== '1') {
+						name = t('activity', '{name} (restricted)', parameter);
+					}
+
+					return OCA.Activity.Templates.systemTag({
+						name: name
+					}).trim("\n");
+
+				case 'email':
+					return OCA.Activity.Templates.email(parameter).trim("\n");
+
+				case 'open-graph':
+					return OCA.Activity.Templates.openGraph(parameter).trim("\n");
+
+				case 'user':
+					if (_.isUndefined(parameter.server)) {
+						return OCA.Activity.Templates.userLocal(parameter).trim("\n");
+					}
+
+					return OCA.Activity.Templates.userRemote(parameter).trim("\n");
+
+				default:
+					if (!_.isUndefined(parameter.link)) {
+						return OCA.Activity.Templates.unkownLink(parameter).trim("\n");
+					}
+
+					return OCA.Activity.Templates.unknown(parameter).trim("\n");
+			}
+		},
+
+		/**
+		 * @param {Object} parameter
+		 * @param {string} parameter.type
+		 * @param {string} parameter.id
+		 * @param {string} parameter.name
+		 * @param {string} parameter.path
+		 * @param {string} parameter.link
+		 */
+		parseFileParameter: function(parameter) {
+			if (parameter.path === '') {
+				return OCA.Activity.Templates.fileRoot(_.extend(parameter, {
+					homeTXT: t('activity', 'Home')
+				}));
+			}
+
+			var lastSlashPosition = parameter.path.lastIndexOf('/'),
+				firstSlashPosition = parameter.path.indexOf('/');
+			parameter.path = parameter.path.substring(firstSlashPosition === 0 ? 1 : 0, lastSlashPosition);
+
+			if (!parameter.link) {
+				parameter.link = OC.generateUrl('/f/{fileId}', {fileId: parameter.id});
+			}
+
+			if (parameter.path === '' || parameter.path === '/') {
+				return OCA.Activity.Templates.fileNoPath(parameter);
+			}
+			return OCA.Activity.Templates.file(_.extend(parameter, {
+				title: parameter.path.length === 0 ? '' : t('activity', 'in {path}', parameter)
+			}));
+		}
+	};
+
+})(OC, OCA);
+
+
+(function() {
+  var template = Handlebars.template, templates = OCA.Activity.Templates = OCA.Activity.Templates || {};
+templates['activitytabview'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<div class=\"activity-section\">\n	<div class=\"loading hidden\" style=\"height: 50px\"></div>\n	<div class=\"emptycontent\">\n		<div class=\"icon-activity\"></div>\n		<p>"
+    + alias4(((helper = (helper = helpers.emptyMessage || (depth0 != null ? depth0.emptyMessage : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"emptyMessage","hash":{},"data":data}) : helper)))
+    + "</p>\n	</div>\n	<ul class=\"activities hidden\">\n	</ul>\n	<input type=\"button\" class=\"showMore\" value=\""
+    + alias4(((helper = (helper = helpers.moreLabel || (depth0 != null ? depth0.moreLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"moreLabel","hash":{},"data":data}) : helper)))
+    + "\">\n</div>\n";
+},"useData":true});
+templates['activitytabview_activity'] = template({"1":function(container,depth0,helpers,partials,data) {
+    return " monochrome";
+},"3":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "			<img src=\""
+    + container.escapeExpression(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"icon","hash":{},"data":data}) : helper)))
+    + "\" alt=\"\">\n";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<li class=\"activity box\">\n	<div class=\"activity-icon"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.isMonochromeIcon : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "\">\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.icon : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "	</div>\n	<div class=\"activitysubject\">"
+    + ((stack1 = ((helper = (helper = helpers.subject || (depth0 != null ? depth0.subject : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"subject","hash":{},"data":data}) : helper))) != null ? stack1 : "")
+    + "</div>\n	<span class=\"activitytime has-tooltip live-relative-timestamp\" data-timestamp=\""
+    + alias4(((helper = (helper = helpers.timestamp || (depth0 != null ? depth0.timestamp : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"timestamp","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.formattedDateTooltip || (depth0 != null ? depth0.formattedDateTooltip : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"formattedDateTooltip","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.formattedDate || (depth0 != null ? depth0.formattedDate : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"formattedDate","hash":{},"data":data}) : helper)))
+    + "</span>\n	<div class=\"activitymessage\">"
+    + ((stack1 = ((helper = (helper = helpers.message || (depth0 != null ? depth0.message : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"message","hash":{},"data":data}) : helper))) != null ? stack1 : "")
+    + "</div>\n</li>\n";
+},"useData":true});
+templates['email'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a class=\"email\" href=\"mailto:"
+    + alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</a>\n";
+},"useData":true});
+templates['file'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a class=\"filename has-tooltip\" href=\""
+    + alias4(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"link","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</a>\n";
+},"useData":true});
+templates['fileNoPath'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a class=\"filename\" href=\""
+    + alias4(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"link","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</a>\n";
+},"useData":true});
+templates['fileRoot'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a class=\"filename has-tooltip\" href=\""
+    + alias4(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"link","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.homeTXT || (depth0 != null ? depth0.homeTXT : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"homeTXT","hash":{},"data":data}) : helper)))
+    + "\"><span class=\"icon icon-home\"></span></a>\n";
+},"useData":true});
+templates['openGraph'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "	<a href=\""
+    + container.escapeExpression(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"link","hash":{},"data":data}) : helper)))
+    + "\">\n";
+},"3":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "			<div class=\"opengraph-thumb\" style=\"background-image: url('"
+    + container.escapeExpression(((helper = (helper = helpers.thumb || (depth0 != null ? depth0.thumb : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"thumb","hash":{},"data":data}) : helper)))
+    + "')\"></div>\n";
+},"5":function(container,depth0,helpers,partials,data) {
+    return "opengraph-with-thumb";
+},"7":function(container,depth0,helpers,partials,data) {
+    return "	</a>\n";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.link : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "	<div id=\"opengraph-"
+    + alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
+    + "\" class=\"opengraph\">\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.thumb : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "		<div class=\"opengraph-name "
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.thumb : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</div>\n		<div class=\"opengraph-description "
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.thumb : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "\">"
+    + alias4(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"description","hash":{},"data":data}) : helper)))
+    + "</div>\n		<span class=\"opengraph-website\">"
+    + alias4(((helper = (helper = helpers.website || (depth0 != null ? depth0.website : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"website","hash":{},"data":data}) : helper)))
+    + "</span>\n	</div>\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.link : depth0),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"useData":true});
+templates['systemTag'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<strong class=\"systemtag\">"
+    + container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"name","hash":{},"data":data}) : helper)))
+    + "</strong>\n";
+},"useData":true});
+templates['unknown'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<strong>"
+    + container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"name","hash":{},"data":data}) : helper)))
+    + "</strong>\n";
+},"useData":true});
+templates['unkownLink'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a href=\""
+    + alias4(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"link","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</a>\n";
+},"useData":true});
+templates['userLocal'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<span class=\"avatar-name-wrapper\" data-user=\""
+    + alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
+    + "\"><div class=\"avatar\" data-user=\""
+    + alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
+    + "\" data-user-display-name=\""
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\"></div><strong>"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</strong></span>\n";
+},"useData":true});
+templates['userRemote'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<strong>"
+    + container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"name","hash":{},"data":data}) : helper)))
+    + "</strong>\n";
+},"useData":true});
+})();
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	/**
+	 * @class OCA.Activity.ActivityModel
+	 * @classdesc
+	 *
+	 * Displays activity information for a given file
+	 *
+	 */
+	var ActivityModel = OC.Backbone.Model.extend(/** @lends OCA.Activity.ActivityModel.prototype */{
+		/**
+		 *
+		 * @returns int UNIX milliseconds timestamp
+		 */
+		getUnixMilliseconds: function () {
+			if (_.isUndefined(this.unixMilliseconds)) {
+				this.unixMilliseconds = moment(this.get('datetime')).valueOf();
+			}
+			return this.unixMilliseconds;
+		},
+
+		/**
+		 * @returns string E.g. "seconds ago"
+		 */
+		getRelativeDate: function () {
+			return OC.Util.relativeModifiedDate(this.getUnixMilliseconds());
+		},
+
+		/**
+		 * @returns string E.g. "April 26, 2016 10:53 AM"
+		 */
+		getFullDate: function () {
+			return OC.Util.formatDate(this.getUnixMilliseconds());
+		},
+
+		/**
+		 * @returns bool
+		 */
+		isMonochromeIcon: function () {
+			return this.get('type') !== 'file_created' && this.get('type') !== 'file_deleted' && this.get('type') !== 'favorite';
+		}
+	});
+
+	OCA.Activity = OCA.Activity || {};
+	OCA.Activity.ActivityModel = ActivityModel;
+})();
+
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	OCA.Activity = OCA.Activity || {};
+
+	/**
+	 * @class OCA.Activity.ActivityCollection
+	 * @classdesc
+	 *
+	 * Displays activity information for a given file
+	 *
+	 */
+	var ActivityCollection = OC.Backbone.Collection.extend(
+		/** @lends OCA.Activity.ActivityCollection.prototype */ {
+
+		firstKnownId: 0,
+		lastGivenId: 0,
+		hasMore: false,
+
+		/**
+		 * Id of the file for which to filter activities by
+		 *
+		 * @var int
+		 */
+		_objectId: null,
+
+		/**
+		 * Type of the object to filter by
+		 *
+		 * @var string
+		 */
+		_objectType: null,
+
+		model: OCA.Activity.ActivityModel,
+
+		/**
+		 * Sets the object id to filter by or null for all.
+		 * 
+		 * @param {int} objectId file id or null
+		 */
+		setObjectId: function(objectId) {
+			this._objectId = objectId;
+			this.firstKnownId = 0;
+			this.lastGivenId = 0;
+			this.hasMore = false;
+		},
+
+		/**
+		 * Sets the object type to filter by or null for all.
+		 * 
+		 * @param {string} objectType string
+		 */
+		setObjectType: function(objectType) {
+			this._objectType = objectType;
+			this.firstKnownId = 0;
+			this.lastGivenId = 0;
+			this.hasMore = false;
+		},
+
+		/**
+		 *
+		 * @param ocsResponse
+		 * @param response
+		 * @returns {Array}
+		 */
+		parse: function(ocsResponse, response) {
+			this._saveHeaders(response.xhr.getAllResponseHeaders());
+
+			if (response.xhr.status === 304) {
+				// No activities found
+				return [];
+			}
+
+			return ocsResponse.ocs.data;
+		},
+
+		/**
+		 * Read the X-Activity-First-Known and X-Activity-Last-Given headers
+		 * @param headers
+		 */
+		_saveHeaders: function(headers) {
+			var self = this;
+			this.hasMore = false;
+
+			headers = headers.split("\n");
+			_.each(headers, function (header) {
+				var parts = header.split(':');
+				if (parts[0].toLowerCase() === 'x-activity-first-known') {
+					self.firstKnownId = parseInt(parts[1].trim(), 10);
+				} else if (parts[0].toLowerCase() === 'x-activity-last-given') {
+					self.lastGivenId = parseInt(parts[1].trim(), 10);
+				} else if (parts[0].toLowerCase() === 'link') {
+					self.hasMore = true;
+				}
+			});
+		},
+
+		url: function() {
+			var query = {
+				format: 'json'
+			};
+			var url = OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'filter';
+			if (this.lastGivenId) {
+				query.since = this.lastGivenId;
+			}
+			if (this._objectId && this._objectType) {
+				query.object_type = this._objectType;
+				query.object_id = this._objectId;
+			}
+			url += '?' + OC.buildQueryString(query);
+			return url;
+		}
+	});
+
+	OCA.Activity.ActivityCollection = ActivityCollection;
+})();
+
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	/**
+	 * @class OCA.Activity.ActivityTabView
+	 * @classdesc
+	 *
+	 * Displays activity information for a given file
+	 *
+	 */
+	var ActivityTabView = OCA.Files.DetailTabView.extend(/** @lends OCA.Activity.ActivityTabView.prototype */ {
+		id: 'activityTabView',
+		className: 'activityTabView tab',
+
+		events: {
+			'click .showMore': '_onClickShowMore'
+		},
+
+		_loading: false,
+		_plugins: [],
+
+		initialize: function() {
+			this.collection = new OCA.Activity.ActivityCollection();
+			this.collection.setObjectType('files');
+			this.collection.on('request', this._onRequest, this);
+			this.collection.on('sync', this._onEndRequest, this);
+			this.collection.on('error', this._onError, this);
+			this.collection.on('add', this._onAddModel, this);
+
+			this._plugins = OC.Plugins.getPlugins('OCA.Activity.RenderingPlugins');
+			_.each(this._plugins, function(plugin) {
+				if (_.isFunction(plugin.initialize)) {
+					plugin.initialize();
+				}
+			});
+		},
+
+		template: function(data) {
+			return OCA.Activity.Templates['activitytabview'](data);
+		},
+
+		get$: function() {
+			return this.$el;
+		},
+
+		getLabel: function() {
+			return t('activity', 'Activity');
+		},
+
+		getIcon: function() {
+			return 'icon-activity';
+		},
+
+		setFileInfo: function(fileInfo) {
+			this._fileInfo = fileInfo;
+			if (this._fileInfo) {
+				this.collection.setObjectId(this._fileInfo.get('id'));
+				this.collection.reset();
+				this.collection.fetch();
+
+				_.each(this._plugins, function(plugin) {
+					if (_.isFunction(plugin.setFileInfo)) {
+						plugin.setFileInfo('files', fileInfo.get('id'));
+					}
+				});
+			} else {
+				this.collection.reset();
+
+				_.each(this._plugins, function(plugin) {
+					if (_.isFunction(plugin.resetFileInfo)) {
+						plugin.resetFileInfo();
+					}
+				});
+			}
+		},
+
+		_onError: function() {
+			var $emptyContent = this.$el.find('.emptycontent');
+			$emptyContent.removeClass('hidden');
+			$emptyContent.find('p').text(t('activity', 'An error occurred while loading activities'));
+		},
+
+		_onRequest: function() {
+			if (this.collection.lastGivenId === 0) {
+				this.render();
+			}
+			this.$el.find('.showMore').addClass('hidden');
+		},
+
+		_onEndRequest: function() {
+			this.$container.removeClass('hidden');
+			this.$el.find('.loading').addClass('hidden');
+			if (this.collection.length) {
+				this.$el.find('.emptycontent').addClass('hidden');
+			}
+			if (this.collection.hasMore) {
+				this.$el.find('.showMore').removeClass('hidden');
+			}
+		},
+
+		_onClickShowMore: function() {
+			this.collection.fetch({
+				reset: false
+			});
+		},
+
+		/**
+		 * Format an activity model for display
+		 *
+		 * @param {OCA.Activity.ActivityModel} activity
+		 * @return {Object}
+		 */
+		_formatItem: function(activity) {
+
+			var subject = escapeHTML(activity.get('subject')),
+				subject_rich = activity.get('subject_rich');
+			if (subject_rich[0].length > 1) {
+				subject = OCA.Activity.RichObjectStringParser.parseMessage(subject_rich[0], subject_rich[1]);
+			}
+			var message = escapeHTML(activity.get('message')),
+				message_rich = activity.get('message_rich');
+			if (message_rich[0].length > 1) {
+				message = OCA.Activity.RichObjectStringParser.parseMessage(message_rich[0], message_rich[1]);
+			}
+
+			var output = {
+				subject: subject,
+				formattedDate: activity.getRelativeDate(),
+				formattedDateTooltip: activity.getFullDate(),
+				isMonochromeIcon: activity.isMonochromeIcon(),
+				timestamp: moment(activity.get('datetime')).valueOf(),
+				message: message,
+				icon: activity.get('icon')
+			};
+
+			/**
+			 * Disable previews in the rightside bar,
+			 * it's always the same image anyway.
+			 if (activity.has('previews')) {
+					output.previews = _.map(activity.get('previews'), function(data) {
+						return {
+							previewClass: data.isMimeTypeIcon ? 'preview-mimetype-icon': '',
+							source: data.source
+						};
+					});
+				}
+			 */
+			return output;
+		},
+
+		activityTemplate: function(params) {
+			return OCA.Activity.Templates['activitytabview_activity'](params);
+		},
+
+		_onAddModel: function(model, collection, options) {
+			var $el = $(this.activityTemplate(this._formatItem(model)));
+
+			_.each(this._plugins, function(plugin) {
+				if (_.isFunction(plugin.prepareModelForDisplay)) {
+					plugin.prepareModelForDisplay(model, $el, 'ActivityTabView');
+				}
+			});
+
+			if (!_.isUndefined(options.at) && collection.length > 1) {
+				this.$container.find('li').eq(options.at).before($el);
+			} else {
+				this.$container.append($el);
+			}
+
+			this._postRenderItem($el);
+		},
+
+		_postRenderItem: function($el) {
+			$el.find('.avatar').each(function() {
+				var element = $(this);
+				if (element.data('user-display-name')) {
+					element.avatar(element.data('user'), 21, undefined, false, undefined, element.data('user-display-name'));
+				} else {
+					element.avatar(element.data('user'), 21);
+				}
+			});
+			$el.find('.avatar-name-wrapper').each(function() {
+				var element = $(this);
+				var avatar = element.find('.avatar');
+				var label = element.find('strong');
+
+				$.merge(avatar, label).contactsMenu(element.data('user'), 0, element);
+			});
+			$el.find('.has-tooltip').tooltip({
+				placement: 'bottom'
+			});
+		},
+
+
+		/**
+		 * Renders this details view
+		 */
+		render: function() {
+			if (this._fileInfo) {
+				this.$el.html(this.template({
+					emptyMessage: t('activity', 'No activity yet'),
+					moreLabel: t('activity', 'Load more activities')
+				}));
+				this.$container = this.$el.find('ul.activities');
+			}
+		}
+	});
+
+	OCA.Activity = OCA.Activity || {};
+	OCA.Activity.ActivityTabView = ActivityTabView;
+})();
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OCA) {
+
+var FilesPlugin = {
+	attach: function(fileList) {
+		fileList.registerTabView(new OCA.Activity.ActivityTabView({order: -50}));
+	}
+};
+
+OC.Plugins.register('OCA.Files.FileList', FilesPlugin);
+
+})(OCA);
+
+
+

+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/apps\/activity\/js\/activity-sidebar.json":1570177632,"\/var\/www\/html\/apps\/activity\/js\/richObjectStringParser.js":1570177632,"\/var\/www\/html\/apps\/activity\/js\/templates.js":1570177632,"\/var\/www\/html\/apps\/activity\/js\/activitymodel.js":1570177632,"\/var\/www\/html\/apps\/activity\/js\/activitycollection.js":1570177632,"\/var\/www\/html\/apps\/activity\/js\/activitytabview.js":1570177632,"\/var\/www\/html\/apps\/activity\/js\/filesplugin.js":1570177632}

BIN
compose/karmen/data/appdata_occakkp6o8zz/js/activity/activity-sidebar.js.gzip


+ 12430 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js

@@ -0,0 +1,12430 @@
+/*
+ * Copyright (c) 2014
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global dragOptions, folderDropOptions, OC */
+(function() {
+
+	if (!OCA.Files) {
+		/**
+		 * Namespace for the files app
+		 * @namespace OCA.Files
+		 */
+		OCA.Files = {};
+	}
+
+	/**
+	 * @namespace OCA.Files.App
+	 */
+	OCA.Files.App = {
+		/**
+		 * Navigation control
+		 *
+		 * @member {OCA.Files.Navigation}
+		 */
+		navigation: null,
+
+		/**
+		 * File list for the "All files" section.
+		 *
+		 * @member {OCA.Files.FileList}
+		 */
+		fileList: null,
+
+		/**
+		 * Backbone model for storing files preferences
+		 */
+		_filesConfig: null,
+
+		/**
+		 * Initializes the files app
+		 */
+		initialize: function() {
+			this.navigation = new OCA.Files.Navigation($('#app-navigation'));
+			this.$showHiddenFiles = $('input#showhiddenfilesToggle');
+			var showHidden = $('#showHiddenFiles').val() === "1";
+			this.$showHiddenFiles.prop('checked', showHidden);
+
+
+			if ($('#fileNotFound').val() === "1") {
+				OC.Notification.show(t('files', 'File could not be found'), {type: 'error'});
+			}
+
+			this._filesConfig = new OC.Backbone.Model({
+				showhidden: showHidden
+			});
+
+			var urlParams = OC.Util.History.parseUrlQuery();
+			var fileActions = new OCA.Files.FileActions();
+			// default actions
+			fileActions.registerDefaultActions();
+			// legacy actions
+			fileActions.merge(window.FileActions);
+			// regular actions
+			fileActions.merge(OCA.Files.fileActions);
+
+			this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
+			OCA.Files.fileActions.on('setDefault.app-files', this._onActionsUpdated);
+			OCA.Files.fileActions.on('registerAction.app-files', this._onActionsUpdated);
+			window.FileActions.on('setDefault.app-files', this._onActionsUpdated);
+			window.FileActions.on('registerAction.app-files', this._onActionsUpdated);
+
+			this.files = OCA.Files.Files;
+
+			// TODO: ideally these should be in a separate class / app (the embedded "all files" app)
+			this.fileList = new OCA.Files.FileList(
+				$('#app-content-files'), {
+					dragOptions: dragOptions,
+					folderDropOptions: folderDropOptions,
+					fileActions: fileActions,
+					allowLegacyActions: true,
+					scrollTo: urlParams.scrollto,
+					filesClient: OC.Files.getClient(),
+					multiSelectMenu: [
+						{
+							name: 'copyMove',
+							displayName:  t('files', 'Move or copy'),
+							iconClass: 'icon-external',
+						},
+						{
+							name: 'download',
+							displayName:  t('files', 'Download'),
+							iconClass: 'icon-download',
+						},
+						OCA.Files.FileList.MultiSelectMenuActions.ToggleSelectionModeAction,
+						{
+							name: 'delete',
+							displayName: t('files', 'Delete'),
+							iconClass: 'icon-delete',
+						},
+					],
+					sorting: {
+						mode: $('#defaultFileSorting').val(),
+						direction: $('#defaultFileSortingDirection').val()
+					},
+					config: this._filesConfig,
+					enableUpload: true,
+					maxChunkSize: OC.appConfig.files && OC.appConfig.files.max_chunk_size
+				}
+			);
+			this.files.initialize();
+
+			// for backward compatibility, the global FileList will
+			// refer to the one of the "files" view
+			window.FileList = this.fileList;
+
+			OC.Plugins.attach('OCA.Files.App', this);
+
+			this._setupEvents();
+			// trigger URL change event handlers
+			this._onPopState(urlParams);
+
+			$('#quota.has-tooltip').tooltip({
+				placement: 'top'
+			});
+
+			this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
+
+			if (sessionStorage.getItem('WhatsNewServerCheck') < (Date.now() - 3600*1000)) {
+				OCP.WhatsNew.query(); // for Nextcloud server
+				sessionStorage.setItem('WhatsNewServerCheck', Date.now());
+			}
+		},
+
+		/**
+		 * Destroy the app
+		 */
+		destroy: function() {
+			this.navigation = null;
+			this.fileList.destroy();
+			this.fileList = null;
+			this.files = null;
+			OCA.Files.fileActions.off('setDefault.app-files', this._onActionsUpdated);
+			OCA.Files.fileActions.off('registerAction.app-files', this._onActionsUpdated);
+			window.FileActions.off('setDefault.app-files', this._onActionsUpdated);
+			window.FileActions.off('registerAction.app-files', this._onActionsUpdated);
+		},
+
+		_onActionsUpdated: function(ev) {
+			// forward new action to the file list
+			if (ev.action) {
+				this.fileList.fileActions.registerAction(ev.action);
+			} else if (ev.defaultAction) {
+				this.fileList.fileActions.setDefault(
+					ev.defaultAction.mime,
+					ev.defaultAction.name
+				);
+			}
+		},
+
+		/**
+		 * Returns the container of the currently visible app.
+		 *
+		 * @return app container
+		 */
+		getCurrentAppContainer: function() {
+			return this.navigation.getActiveContainer();
+		},
+
+		/**
+		 * Sets the currently active view
+		 * @param viewId view id
+		 */
+		setActiveView: function(viewId, options) {
+			this.navigation.setActiveItem(viewId, options);
+		},
+
+		/**
+		 * Returns the view id of the currently active view
+		 * @return view id
+		 */
+		getActiveView: function() {
+			return this.navigation.getActiveItem();
+		},
+
+		/**
+		 *
+		 * @returns {Backbone.Model}
+		 */
+		getFilesConfig: function() {
+			return this._filesConfig;
+		},
+
+		/**
+		 * Setup events based on URL changes
+		 */
+		_setupEvents: function() {
+			OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
+
+			// detect when app changed their current directory
+			$('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this));
+			$('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this));
+			$('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this));
+
+			$('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this));
+			this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this));
+		},
+
+		/**
+		 * Toggle showing hidden files according to the settings checkbox
+		 *
+		 * @returns {undefined}
+		 */
+		_onShowHiddenFilesChange: function() {
+			var show = this.$showHiddenFiles.is(':checked');
+			this._filesConfig.set('showhidden', show);
+			this._debouncedPersistShowHiddenFilesState();
+		},
+
+		/**
+		 * Persist show hidden preference on the server
+		 *
+		 * @returns {undefined}
+		 */
+		_persistShowHiddenFilesState: function() {
+			var show = this._filesConfig.get('showhidden');
+			$.post(OC.generateUrl('/apps/files/api/v1/showhidden'), {
+				show: show
+			});
+		},
+
+		/**
+		 * Event handler for when the current navigation item has changed
+		 */
+		_onNavigationChanged: function(e) {
+			var params;
+			if (e && e.itemId) {
+				params = {
+					view: typeof e.view === 'string' && e.view !== '' ? e.view : e.itemId,
+					dir: e.dir ? e.dir : '/'
+				};
+				this._changeUrl(params.view, params.dir);
+				OC.Apps.hideAppSidebar($('.detailsView'));
+				this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
+			}
+		},
+
+		/**
+		 * Event handler for when an app notified that its directory changed
+		 */
+		_onDirectoryChanged: function(e) {
+			if (e.dir) {
+				this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
+			}
+		},
+
+		/**
+		 * Event handler for when an app notified that its directory changed
+		 */
+		_onAfterDirectoryChanged: function(e) {
+			if (e.dir && e.fileId) {
+				this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
+			}
+		},
+
+		/**
+		 * Event handler for when an app notifies that it needs space
+		 * for viewer mode.
+		 */
+		_onChangeViewerMode: function(e) {
+			var state = !!e.viewerModeEnabled;
+			if (e.viewerModeEnabled) {
+				OC.Apps.hideAppSidebar($('.detailsView'));
+			}
+			$('#app-navigation').toggleClass('hidden', state);
+			$('.app-files').toggleClass('viewer-mode no-sidebar', state);
+		},
+
+		/**
+		 * Event handler for when the URL changed
+		 */
+		_onPopState: function(params) {
+			params = _.extend({
+				dir: '/',
+				view: 'files'
+			}, params);
+			var lastId = this.navigation.getActiveItem();
+			if (!this.navigation.itemExists(params.view)) {
+				params.view = 'files';
+			}
+			this.navigation.setActiveItem(params.view, {silent: true});
+			if (lastId !== this.navigation.getActiveItem()) {
+				this.navigation.getActiveContainer().trigger(new $.Event('show'));
+			}
+			this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
+		},
+
+		/**
+		 * Encode URL params into a string, except for the "dir" attribute
+		 * that gets encoded as path where "/" is not encoded
+		 *
+		 * @param {Object.<string>} params
+		 * @return {string} encoded params
+		 */
+		_makeUrlParams: function(params) {
+			var dir = params.dir;
+			delete params.dir;
+			return 'dir=' + OC.encodePath(dir) + '&' + OC.buildQueryString(params);
+		},
+
+		/**
+		 * Change the URL to point to the given dir and view
+		 */
+		_changeUrl: function(view, dir, fileId) {
+			var params = {dir: dir};
+			if (view !== 'files') {
+				params.view = view;
+			} else if (fileId) {
+				params.fileid = fileId;
+			}
+			var currentParams = OC.Util.History.parseUrlQuery();
+			if (currentParams.dir === params.dir && currentParams.view === params.view && currentParams.fileid !== params.fileid) {
+				// if only fileid changed or was added, replace instead of push
+				OC.Util.History.replaceState(this._makeUrlParams(params));
+			} else {
+				OC.Util.History.pushState(this._makeUrlParams(params));
+			}
+		}
+	};
+})();
+
+$(document).ready(function() {
+	// wait for other apps/extensions to register their event handlers and file actions
+	// in the "ready" clause
+	_.defer(function() {
+		OCA.Files.App.initialize();
+	});
+});
+
+
+(function() {
+  var template = Handlebars.template, templates = OCA.Files.Templates = OCA.Files.Templates || {};
+templates['detailsview'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var stack1;
+
+  return "<ul class=\"tabHeaders\">\n"
+    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.tabHeaders : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "</ul>\n";
+},"2":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "	<li class=\"tabHeader\" data-tabid=\""
+    + alias4(((helper = (helper = helpers.tabId || (depth0 != null ? depth0.tabId : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"tabId","hash":{},"data":data}) : helper)))
+    + "\" tabindex=\"0\">\n	    "
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tabIcon : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "\n		<a href=\"#\" tabindex=\"-1\">"
+    + alias4(((helper = (helper = helpers.label || (depth0 != null ? depth0.label : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"label","hash":{},"data":data}) : helper)))
+    + "</a>\n	</li>\n";
+},"3":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<span class=\"icon "
+    + container.escapeExpression(((helper = (helper = helpers.tabIcon || (depth0 != null ? depth0.tabIcon : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"tabIcon","hash":{},"data":data}) : helper)))
+    + "\"></span>";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
+
+  return "<div class=\"detailFileInfoContainer\"></div>\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.tabHeaders : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "<div class=\"tabsContainer\"></div>\n<a class=\"close icon-close\" href=\"#\"><span class=\"hidden-visually\">"
+    + container.escapeExpression(((helper = (helper = helpers.closeLabel || (depth0 != null ? depth0.closeLabel : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"closeLabel","hash":{},"data":data}) : helper)))
+    + "</span></a>\n";
+},"useData":true});
+templates['favorite_mark'] = template({"1":function(container,depth0,helpers,partials,data) {
+    return "permanent";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, options, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression, buffer = 
+  "<div class=\"favorite-mark ";
+  stack1 = ((helper = (helper = helpers.isFavorite || (depth0 != null ? depth0.isFavorite : depth0)) != null ? helper : alias2),(options={"name":"isFavorite","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data}),(typeof helper === alias3 ? helper.call(alias1,options) : helper));
+  if (!helpers.isFavorite) { stack1 = helpers.blockHelperMissing.call(depth0,stack1,options)}
+  if (stack1 != null) { buffer += stack1; }
+  return buffer + "\">\n	<span class=\"icon "
+    + alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + "\" />\n	<span class=\"hidden-visually\">"
+    + alias4(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altText","hash":{},"data":data}) : helper)))
+    + "</span>\n</div>\n";
+},"useData":true});
+templates['file_action_trigger'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "		<img class=\"svg\" alt=\""
+    + alias4(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altText","hash":{},"data":data}) : helper)))
+    + "\" src=\""
+    + alias4(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"icon","hash":{},"data":data}) : helper)))
+    + "\" />\n";
+},"3":function(container,depth0,helpers,partials,data) {
+    var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
+
+  return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasDisplayName : depth0),{"name":"unless","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
+},"4":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "			<span class=\"icon "
+    + container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + "\" />\n";
+},"6":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "			<span class=\"hidden-visually\">"
+    + container.escapeExpression(((helper = (helper = helpers.altText || (depth0 != null ? depth0.altText : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"altText","hash":{},"data":data}) : helper)))
+    + "</span>\n";
+},"8":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<span> "
+    + container.escapeExpression(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"displayName","hash":{},"data":data}) : helper)))
+    + "</span>";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a class=\"action action-"
+    + alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data}) : helper)))
+    + "\" href=\"#\" data-action=\""
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\">\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.icon : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.program(3, data, 0),"data":data})) != null ? stack1 : "")
+    + "	"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.displayName : depth0),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "\n</a>\n";
+},"useData":true});
+templates['fileactionsmenu'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "		<li class=\""
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.inline : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + " action-"
+    + alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data}) : helper)))
+    + "-container\">\n			<a href=\"#\" class=\"menuitem action action-"
+    + alias4(((helper = (helper = helpers.nameLowerCase || (depth0 != null ? depth0.nameLowerCase : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"nameLowerCase","hash":{},"data":data}) : helper)))
+    + " permanent\" data-action=\""
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\">\n				"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.icon : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.program(6, data, 0),"data":data})) != null ? stack1 : "")
+    + "				<span>"
+    + alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data}) : helper)))
+    + "</span>\n			</a>\n		</li>\n";
+},"2":function(container,depth0,helpers,partials,data) {
+    return "hidden";
+},"4":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<img class=\"icon\" src=\""
+    + container.escapeExpression(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"icon","hash":{},"data":data}) : helper)))
+    + "\"/>\n";
+},"6":function(container,depth0,helpers,partials,data) {
+    var stack1;
+
+  return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.program(9, data, 0),"data":data})) != null ? stack1 : "");
+},"7":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "						<span class=\"icon "
+    + container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + "\"></span>\n";
+},"9":function(container,depth0,helpers,partials,data) {
+    return "						<span class=\"no-icon\"></span>\n";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1;
+
+  return "<ul>\n"
+    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "</ul>\n";
+},"useData":true});
+templates['filemultiselectmenu'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "		<li class=\"item-"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\">\n			<a href=\"#\" class=\"menuitem action "
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + " permanent\" data-action=\""
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\">\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.iconClass : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "")
+    + "				<span class=\"label\">"
+    + alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data}) : helper)))
+    + "</span>\n			</a>\n		</li>\n";
+},"2":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "					<span class=\"icon "
+    + container.escapeExpression(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + "\"></span>\n";
+},"4":function(container,depth0,helpers,partials,data) {
+    return "					<span class=\"no-icon\"></span>\n";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1;
+
+  return "<ul>\n"
+    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "</ul>\n";
+},"useData":true});
+templates['filesummary'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<span class=\"info\">\n	<span class=\"dirinfo\"></span>\n	<span class=\"connector\">"
+    + container.escapeExpression(((helper = (helper = helpers.connectorLabel || (depth0 != null ? depth0.connectorLabel : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"connectorLabel","hash":{},"data":data}) : helper)))
+    + "</span>\n	<span class=\"fileinfo\"></span>\n	<span class=\"hiddeninfo\"></span>\n	<span class=\"filter\"></span>\n</span>\n";
+},"useData":true});
+templates['mainfileinfodetailsview'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "			<a href=\"#\" class=\"action action-favorite favorite permanent\">\n				<span class=\"icon "
+    + alias4(((helper = (helper = helpers.starClass || (depth0 != null ? depth0.starClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"starClass","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.starAltText || (depth0 != null ? depth0.starAltText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"starAltText","hash":{},"data":data}) : helper)))
+    + "\"></span>\n			</a>\n";
+},"3":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<span class=\"size\" title=\""
+    + alias4(((helper = (helper = helpers.altSize || (depth0 != null ? depth0.altSize : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altSize","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.size || (depth0 != null ? depth0.size : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"size","hash":{},"data":data}) : helper)))
+    + "</span>, ";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<div class=\"thumbnailContainer\"><a href=\"#\" class=\"thumbnail action-default\"><div class=\"stretcher\"/></a></div>\n<div class=\"file-details-container\">\n	<div class=\"fileName\">\n		<h3 title=\""
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "\" class=\"ellipsis\">"
+    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
+    + "</h3>\n		<a class=\"permalink\" href=\""
+    + alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data}) : helper)))
+    + "\" data-clipboard-text=\""
+    + alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data}) : helper)))
+    + "\">\n			<span class=\"icon icon-clippy\"></span>\n			<span class=\"hidden-visually\">"
+    + alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data}) : helper)))
+    + "</span>\n		</a>\n	</div>\n	<div class=\"file-details ellipsis\">\n"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasFavoriteAction : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "		"
+    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.hasSize : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "<span class=\"date live-relative-timestamp\" data-timestamp=\""
+    + alias4(((helper = (helper = helpers.timestamp || (depth0 != null ? depth0.timestamp : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"timestamp","hash":{},"data":data}) : helper)))
+    + "\" title=\""
+    + alias4(((helper = (helper = helpers.altDate || (depth0 != null ? depth0.altDate : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"altDate","hash":{},"data":data}) : helper)))
+    + "\">"
+    + alias4(((helper = (helper = helpers.date || (depth0 != null ? depth0.date : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"date","hash":{},"data":data}) : helper)))
+    + "</span>\n	</div>\n</div>\n<div class=\"hidden permalink-field\">\n	<input type=\"text\" value=\""
+    + alias4(((helper = (helper = helpers.permalink || (depth0 != null ? depth0.permalink : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalink","hash":{},"data":data}) : helper)))
+    + "\" placeholder=\""
+    + alias4(((helper = (helper = helpers.permalinkTitle || (depth0 != null ? depth0.permalinkTitle : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"permalinkTitle","hash":{},"data":data}) : helper)))
+    + "\" readonly=\"readonly\"/>\n</div>\n";
+},"useData":true});
+templates['newfilemenu'] = template({"1":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "		<li>\n			<a href=\"#\" class=\"menuitem\" data-templatename=\""
+    + alias4(((helper = (helper = helpers.templateName || (depth0 != null ? depth0.templateName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"templateName","hash":{},"data":data}) : helper)))
+    + "\" data-filetype=\""
+    + alias4(((helper = (helper = helpers.fileType || (depth0 != null ? depth0.fileType : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileType","hash":{},"data":data}) : helper)))
+    + "\" data-action=\""
+    + alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
+    + "\"><span class=\"icon "
+    + alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + " svg\"></span><span class=\"displayname\">"
+    + alias4(((helper = (helper = helpers.displayName || (depth0 != null ? depth0.displayName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"displayName","hash":{},"data":data}) : helper)))
+    + "</span></a>\n		</li>\n";
+},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<ul>\n	<li>\n		<label for=\"file_upload_start\" class=\"menuitem\" data-action=\"upload\" title=\""
+    + alias4(((helper = (helper = helpers.uploadMaxHumanFilesize || (depth0 != null ? depth0.uploadMaxHumanFilesize : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"uploadMaxHumanFilesize","hash":{},"data":data}) : helper)))
+    + "\" tabindex=\"0\"><span class=\"svg icon icon-upload\"></span><span class=\"displayname\">"
+    + alias4(((helper = (helper = helpers.uploadLabel || (depth0 != null ? depth0.uploadLabel : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"uploadLabel","hash":{},"data":data}) : helper)))
+    + "</span></label>\n	</li>\n"
+    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+    + "</ul>\n";
+},"useData":true});
+templates['newfilemenu_filename_form'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<form class=\"filenameform\">\n	<input id=\""
+    + alias4(((helper = (helper = helpers.cid || (depth0 != null ? depth0.cid : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"cid","hash":{},"data":data}) : helper)))
+    + "-input-"
+    + alias4(((helper = (helper = helpers.fileType || (depth0 != null ? depth0.fileType : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileType","hash":{},"data":data}) : helper)))
+    + "\" type=\"text\" value=\""
+    + alias4(((helper = (helper = helpers.fileName || (depth0 != null ? depth0.fileName : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"fileName","hash":{},"data":data}) : helper)))
+    + "\" autocomplete=\"off\" autocapitalize=\"off\">\n	<input type=\"submit\" value=\" \" class=\"icon-confirm\" />\n</form>\n";
+},"useData":true});
+templates['operationprogressbar'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper;
+
+  return "<div id=\"uploadprogressbar\">\n	<em class=\"label outer\" style=\"display:none\"></em>\n</div>\n<button class=\"stop icon-close\" style=\"display:none\">\n	<span class=\"hidden-visually\">"
+    + container.escapeExpression(((helper = (helper = helpers.textCancelButton || (depth0 != null ? depth0.textCancelButton : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"textCancelButton","hash":{},"data":data}) : helper)))
+    + "</span>\n</button>\n";
+},"useData":true});
+templates['operationprogressbarlabel'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<em class=\"label\">\n	<span class=\"desktop\">"
+    + alias4(((helper = (helper = helpers.textDesktop || (depth0 != null ? depth0.textDesktop : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textDesktop","hash":{},"data":data}) : helper)))
+    + "</span>\n	<span class=\"mobile\">"
+    + alias4(((helper = (helper = helpers.textMobile || (depth0 != null ? depth0.textMobile : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"textMobile","hash":{},"data":data}) : helper)))
+    + "</span>\n</em>\n";
+},"useData":true});
+templates['template_addbutton'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
+    var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
+
+  return "<a href=\"#\" class=\"button new\">\n	<span class=\"icon "
+    + alias4(((helper = (helper = helpers.iconClass || (depth0 != null ? depth0.iconClass : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"iconClass","hash":{},"data":data}) : helper)))
+    + "\"></span>\n	<span class=\"hidden-visually\">"
+    + alias4(((helper = (helper = helpers.addText || (depth0 != null ? depth0.addText : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"addText","hash":{},"data":data}) : helper)))
+    + "</span>\n</a>\n";
+},"useData":true});
+})();
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/**
+ * The file upload code uses several hooks to interact with blueimps jQuery file upload library:
+ * 1. the core upload handling hooks are added when initializing the plugin,
+ * 2. if the browser supports progress events they are added in a separate set after the initialization
+ * 3. every app can add it's own triggers for fileupload
+ *    - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist
+ *    - TODO pictures upload button
+ *    - TODO music upload button
+ */
+
+/* global jQuery, humanFileSize, md5 */
+
+/**
+ * File upload object
+ *
+ * @class OC.FileUpload
+ * @classdesc
+ *
+ * Represents a file upload
+ *
+ * @param {OC.Uploader} uploader uploader
+ * @param {Object} data blueimp data
+ */
+OC.FileUpload = function(uploader, data) {
+	this.uploader = uploader;
+	this.data = data;
+	var basePath = '';
+	if (this.uploader.fileList) {
+		basePath = this.uploader.fileList.getCurrentDirectory();
+	}
+	var path = OC.joinPaths(basePath, this.getFile().relativePath || '', this.getFile().name);
+	this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime();
+};
+OC.FileUpload.CONFLICT_MODE_DETECT = 0;
+OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1;
+OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2;
+OC.FileUpload.prototype = {
+
+	/**
+	 * Unique upload id
+	 *
+	 * @type string
+	 */
+	id: null,
+
+	/**
+	 * Upload element
+	 *
+	 * @type Object
+	 */
+	$uploadEl: null,
+
+	/**
+	 * Target folder
+	 *
+	 * @type string
+	 */
+	_targetFolder: '',
+
+	/**
+	 * @type int
+	 */
+	_conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT,
+
+	/**
+	 * New name from server after autorename
+	 *
+	 * @type String
+	 */
+	_newName: null,
+
+	/**
+	 * Returns the unique upload id
+	 *
+	 * @return string
+	 */
+	getId: function() {
+		return this.id;
+	},
+
+	/**
+	 * Returns the file to be uploaded
+	 *
+	 * @return {File} file
+	 */
+	getFile: function() {
+		return this.data.files[0];
+	},
+
+	/**
+	 * Return the final filename.
+	 *
+	 * @return {String} file name
+	 */
+	getFileName: function() {
+		// autorenamed name
+		if (this._newName) {
+			return this._newName;
+		}
+		return this.getFile().name;
+	},
+
+	setTargetFolder: function(targetFolder) {
+		this._targetFolder = targetFolder;
+	},
+
+	getTargetFolder: function() {
+		return this._targetFolder;
+	},
+
+	/**
+	 * Get full path for the target file, including relative path,
+	 * without the file name.
+	 *
+	 * @return {String} full path
+	 */
+	getFullPath: function() {
+		return OC.joinPaths(this._targetFolder, this.getFile().relativePath || '');
+	},
+
+	/**
+	 * Get full path for the target file,
+	 * including relative path and file name.
+	 *
+	 * @return {String} full path
+	 */
+	getFullFilePath: function() {
+		return OC.joinPaths(this.getFullPath(), this.getFile().name);
+	},
+
+	/**
+	 * Returns conflict resolution mode.
+	 *
+	 * @return {int} conflict mode
+	 */
+	getConflictMode: function() {
+		return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT;
+	},
+
+	/**
+	 * Set conflict resolution mode.
+	 * See CONFLICT_MODE_* constants.
+	 *
+	 * @param {int} mode conflict mode
+	 */
+	setConflictMode: function(mode) {
+		this._conflictMode = mode;
+	},
+
+	deleteUpload: function() {
+		delete this.data.jqXHR;
+	},
+
+	/**
+	 * Trigger autorename and append "(2)".
+	 * Multiple calls will increment the appended number.
+	 */
+	autoRename: function() {
+		var name = this.getFile().name;
+		if (!this._renameAttempt) {
+			this._renameAttempt = 1;
+		}
+
+		var dotPos = name.lastIndexOf('.');
+		var extPart = '';
+		if (dotPos > 0) {
+			this._newName = name.substr(0, dotPos);
+			extPart = name.substr(dotPos);
+		} else {
+			this._newName = name;
+		}
+
+		// generate new name
+		this._renameAttempt++;
+		this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart;
+	},
+
+	/**
+	 * Submit the upload
+	 */
+	submit: function() {
+		var self = this;
+		var data = this.data;
+		var file = this.getFile();
+
+		if (self.aborted === true) {
+			return $.Deferred().resolve().promise();
+		}
+		// it was a folder upload, so make sure the parent directory exists already
+		var folderPromise;
+		if (file.relativePath) {
+			folderPromise = this.uploader.ensureFolderExists(this.getFullPath());
+		} else {
+			folderPromise = $.Deferred().resolve().promise();
+		}
+
+		if (this.uploader.fileList) {
+			this.data.url = this.uploader.fileList.getUploadUrl(this.getFileName(), this.getFullPath());
+		}
+
+		if (!this.data.headers) {
+			this.data.headers = {};
+		}
+
+		// webdav without multipart
+		this.data.multipart = false;
+		this.data.type = 'PUT';
+
+		delete this.data.headers['If-None-Match'];
+		if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT
+			|| this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
+			this.data.headers['If-None-Match'] = '*';
+		}
+
+		var userName = this.uploader.davClient.getUserName();
+		var password = this.uploader.davClient.getPassword();
+		if (userName) {
+			// copy username/password from DAV client
+			this.data.headers['Authorization'] =
+				'Basic ' + btoa(userName + ':' + (password || ''));
+		}
+
+		var chunkFolderPromise;
+		if ($.support.blobSlice
+			&& this.uploader.fileUploadParam.maxChunkSize
+			&& this.getFile().size > this.uploader.fileUploadParam.maxChunkSize
+		) {
+			data.isChunked = true;
+			chunkFolderPromise = this.uploader.davClient.createDirectory(
+				'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
+			);
+			// TODO: if fails, it means same id already existed, need to retry
+		} else {
+			chunkFolderPromise = $.Deferred().resolve().promise();
+		}
+
+		// wait for creation of the required directory before uploading
+		return Promise.all([folderPromise, chunkFolderPromise]).then(function() {
+			if (self.aborted !== true) {
+				data.submit();
+			}
+		}, function() {
+			self.abort();
+		});
+
+	},
+
+	/**
+	 * Process end of transfer
+	 */
+	done: function() {
+		if (!this.data.isChunked) {
+			return $.Deferred().resolve().promise();
+		}
+
+		var uid = OC.getCurrentUser().uid;
+		var mtime = this.getFile().lastModified;
+		var size = this.getFile().size;
+		var headers = {};
+		if (mtime) {
+			headers['X-OC-Mtime'] = mtime / 1000;
+		}
+		if (size) {
+			headers['OC-Total-Length'] = size;
+
+		}
+
+		return this.uploader.davClient.move(
+			'uploads/' + uid + '/' + this.getId() + '/.file',
+			'files/' + uid + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()),
+			true,
+			headers
+		);
+	},
+
+	_deleteChunkFolder: function() {
+		// delete transfer directory for this upload
+		this.uploader.davClient.remove(
+			'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
+		);
+	},
+
+	/**
+	 * Abort the upload
+	 */
+	abort: function() {
+		if (this.data.isChunked) {
+			this._deleteChunkFolder();
+		}
+		this.data.abort();
+		this.deleteUpload();
+		this.aborted = true;
+	},
+
+	/**
+	 * Fail the upload
+	 */
+	fail: function() {
+		this.deleteUpload();
+		if (this.data.isChunked) {
+			this._deleteChunkFolder();
+		}
+	},
+
+	/**
+	 * Returns the server response
+	 *
+	 * @return {Object} response
+	 */
+	getResponse: function() {
+		var response = this.data.response();
+		if (response.errorThrown) {
+			// attempt parsing Sabre exception is available
+			var xml = response.jqXHR.responseXML;
+			if (xml && xml.documentElement.localName === 'error' && xml.documentElement.namespaceURI === 'DAV:') {
+				var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message');
+				var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception');
+				if (messages.length) {
+					response.message = messages[0].textContent;
+				}
+				if (exceptions.length) {
+					response.exception = exceptions[0].textContent;
+				}
+				return response;
+			}
+		}
+
+		if (typeof response.result !== 'string' && response.result) {
+			//fetch response from iframe
+			response = $.parseJSON(response.result[0].body.innerText);
+			if (!response) {
+				// likely due to internal server error
+				response = {status: 500};
+			}
+		} else {
+			response = response.result;
+		}
+		return response;
+	},
+
+	/**
+	 * Returns the status code from the response
+	 *
+	 * @return {int} status code
+	 */
+	getResponseStatus: function() {
+		if (this.uploader.isXHRUpload()) {
+			var xhr = this.data.response().jqXHR;
+			if (xhr) {
+				return xhr.status;
+			}
+			return null;
+		}
+		return this.getResponse().status;
+	},
+
+	/**
+	 * Returns the response header by name
+	 *
+	 * @param {String} headerName header name
+	 * @return {Array|String} response header value(s)
+	 */
+	getResponseHeader: function(headerName) {
+		headerName = headerName.toLowerCase();
+		if (this.uploader.isXHRUpload()) {
+			return this.data.response().jqXHR.getResponseHeader(headerName);
+		}
+
+		var headers = this.getResponse().headers;
+		if (!headers) {
+			return null;
+		}
+
+		var value =  _.find(headers, function(value, key) {
+			return key.toLowerCase() === headerName;
+		});
+		if (_.isArray(value) && value.length === 1) {
+			return value[0];
+		}
+		return value;
+	}
+};
+
+/**
+ * keeps track of uploads in progress and implements callbacks for the conflicts dialog
+ * @namespace
+ */
+
+OC.Uploader = function() {
+	this.init.apply(this, arguments);
+};
+
+OC.Uploader.prototype = _.extend({
+	/**
+	 * @type Array<OC.FileUpload>
+	 */
+	_uploads: {},
+
+	/**
+	 * Count of upload done promises that have not finished yet.
+	 *
+	 * @type int
+	 */
+	_pendingUploadDoneCount: 0,
+
+	/**
+	 * Is it currently uploading?
+	 *
+	 * @type boolean
+	 */
+	_uploading: false,
+
+	/**
+	 * List of directories known to exist.
+	 *
+	 * Key is the fullpath and value is boolean, true meaning that the directory
+	 * was already created so no need to create it again.
+	 */
+	_knownDirs: {},
+
+	/**
+	 * @type OCA.Files.FileList
+	 */
+	fileList: null,
+
+	/**
+	 * @type OCA.Files.OperationProgressBar
+	 */
+	progressBar: null,
+
+	/**
+	 * @type OC.Files.Client
+	 */
+	filesClient: null,
+
+	/**
+	 * Webdav client pointing at the root "dav" endpoint
+	 *
+	 * @type OC.Files.Client
+	 */
+	davClient: null,
+
+	/**
+	 * Function that will allow us to know if Ajax uploads are supported
+	 * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html
+	 * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata
+	 */
+	_supportAjaxUploadWithProgress: function() {
+		if (window.TESTING) {
+			return true;
+		}
+		return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData();
+
+		// Is the File API supported?
+		function supportFileAPI() {
+			var fi = document.createElement('INPUT');
+			fi.type = 'file';
+			return 'files' in fi;
+		}
+
+		// Are progress events supported?
+		function supportAjaxUploadProgressEvents() {
+			var xhr = new XMLHttpRequest();
+			return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload));
+		}
+
+		// Is FormData supported?
+		function supportFormData() {
+			return !! window.FormData;
+		}
+	},
+
+	/**
+	 * Returns whether an XHR upload will be used
+	 *
+	 * @return {bool} true if XHR upload will be used,
+	 * false for iframe upload
+	 */
+	isXHRUpload: function () {
+		return !this.fileUploadParam.forceIframeTransport &&
+			((!this.fileUploadParam.multipart && $.support.xhrFileUpload) ||
+			$.support.xhrFormDataFileUpload);
+	},
+
+	/**
+	 * Makes sure that the upload folder and its parents exists
+	 *
+	 * @param {String} fullPath full path
+	 * @return {Promise} promise that resolves when all parent folders
+	 * were created
+	 */
+	ensureFolderExists: function(fullPath) {
+		if (!fullPath || fullPath === '/') {
+			return $.Deferred().resolve().promise();
+		}
+
+		// remove trailing slash
+		if (fullPath.charAt(fullPath.length - 1) === '/') {
+			fullPath = fullPath.substr(0, fullPath.length - 1);
+		}
+
+		var self = this;
+		var promise = this._knownDirs[fullPath];
+
+		if (this.fileList) {
+			// assume the current folder exists
+			this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise();
+		}
+
+		if (!promise) {
+			var deferred = new $.Deferred();
+			promise = deferred.promise();
+			this._knownDirs[fullPath] = promise;
+
+			// make sure all parents already exist
+			var parentPath = OC.dirname(fullPath);
+			var parentPromise = this._knownDirs[parentPath];
+			if (!parentPromise) {
+				parentPromise = this.ensureFolderExists(parentPath);
+			}
+
+			parentPromise.then(function() {
+				self.filesClient.createDirectory(fullPath).always(function(status) {
+					// 405 is expected if the folder already exists
+					if ((status >= 200 && status < 300) || status === 405) {
+						if (status !== 405) {
+							self.trigger('createdfolder', fullPath);
+						}
+						deferred.resolve();
+						return;
+					}
+					OC.Notification.show(t('files', 'Could not create folder "{dir}"', {dir: fullPath}), {type: 'error'});
+					deferred.reject();
+				});
+			}, function() {
+				deferred.reject();
+			});
+		}
+
+		return promise;
+	},
+
+	/**
+	 * Submit the given uploads
+	 *
+	 * @param {Array} array of uploads to start
+	 */
+	submitUploads: function(uploads) {
+		var self = this;
+		_.each(uploads, function(upload) {
+			self._uploads[upload.data.uploadId] = upload;
+		});
+		self.totalToUpload = _.reduce(uploads, function(memo, upload) { return memo+upload.getFile().size; }, 0);
+		var semaphore = new OCA.Files.Semaphore(5);
+		var promises = _.map(uploads, function(upload) {
+			return semaphore.acquire().then(function(){
+				return upload.submit().then(function(){
+					semaphore.release();
+				});
+			});
+		});
+	},
+
+	confirmBeforeUnload: function() {
+		if (this._uploading) {
+			return t('files', 'This will stop your current uploads.')
+		}
+	},
+
+	/**
+	 * Show conflict for the given file object
+	 *
+	 * @param {OC.FileUpload} file upload object
+	 */
+	showConflict: function(fileUpload) {
+		//show "file already exists" dialog
+		var self = this;
+		var file = fileUpload.getFile();
+		// already attempted autorename but the server said the file exists ? (concurrently added)
+		if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
+			// attempt another autorename, defer to let the current callback finish
+			_.defer(function() {
+				self.onAutorename(fileUpload);
+			});
+			return;
+		}
+		// retrieve more info about this file
+		this.filesClient.getFileInfo(fileUpload.getFullFilePath()).then(function(status, fileInfo) {
+			var original = fileInfo;
+			var replacement = file;
+			original.directory = original.path;
+			OC.dialogs.fileexists(fileUpload, original, replacement, self);
+		});
+	},
+	/**
+	 * cancels all uploads
+	 */
+	cancelUploads:function() {
+		this.log('canceling uploads');
+		jQuery.each(this._uploads, function(i, upload) {
+			upload.abort();
+		});
+		this.clear();
+	},
+	/**
+	 * Clear uploads
+	 */
+	clear: function() {
+		this._knownDirs = {};
+	},
+	/**
+	 * Returns an upload by id
+	 *
+	 * @param {int} data uploadId
+	 * @return {OC.FileUpload} file upload
+	 */
+	getUpload: function(data) {
+		if (_.isString(data)) {
+			return this._uploads[data];
+		} else if (data.uploadId && this._uploads[data.uploadId]) {
+			this._uploads[data.uploadId].data = data;
+			return this._uploads[data.uploadId];
+		}
+		return null;
+	},
+
+	/**
+	 * Removes an upload from the list of known uploads.
+	 *
+	 * @param {OC.FileUpload} upload the upload to remove.
+	 */
+	removeUpload: function(upload) {
+		if (!upload || !upload.data || !upload.data.uploadId) {
+			return;
+		}
+
+		delete this._uploads[upload.data.uploadId];
+	},
+
+	showUploadCancelMessage: _.debounce(function() {
+		OC.Notification.show(t('files', 'Upload cancelled.'), {timeout : 7, type: 'error'});
+	}, 500),
+	/**
+	 * callback for the conflicts dialog
+	 */
+	onCancel:function() {
+		this.cancelUploads();
+	},
+	/**
+	 * callback for the conflicts dialog
+	 * calls onSkip, onReplace or onAutorename for each conflict
+	 * @param {object} conflicts - list of conflict elements
+	 */
+	onContinue:function(conflicts) {
+		var self = this;
+		//iterate over all conflicts
+		jQuery.each(conflicts, function (i, conflict) {
+			conflict = $(conflict);
+			var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1;
+			var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1;
+			if (keepOriginal && keepReplacement) {
+				// when both selected -> autorename
+				self.onAutorename(conflict.data('data'));
+			} else if (keepReplacement) {
+				// when only replacement selected -> overwrite
+				self.onReplace(conflict.data('data'));
+			} else {
+				// when only original selected -> skip
+				// when none selected -> skip
+				self.onSkip(conflict.data('data'));
+			}
+		});
+	},
+	/**
+	 * handle skipping an upload
+	 * @param {OC.FileUpload} upload
+	 */
+	onSkip:function(upload) {
+		this.log('skip', null, upload);
+		upload.deleteUpload();
+	},
+	/**
+	 * handle replacing a file on the server with an uploaded file
+	 * @param {FileUpload} data
+	 */
+	onReplace:function(upload) {
+		this.log('replace', null, upload);
+		upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE);
+		this.submitUploads([upload]);
+	},
+	/**
+	 * handle uploading a file and letting the server decide a new name
+	 * @param {object} upload
+	 */
+	onAutorename:function(upload) {
+		this.log('autorename', null, upload);
+		upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME);
+
+		do {
+			upload.autoRename();
+			// if file known to exist on the client side, retry
+		} while (this.fileList && this.fileList.inList(upload.getFileName()));
+
+		// resubmit upload
+		this.submitUploads([upload]);
+	},
+	_trace: false, //TODO implement log handler for JS per class?
+	log: function(caption, e, data) {
+		if (this._trace) {
+			console.log(caption);
+			console.log(data);
+		}
+	},
+	/**
+	 * checks the list of existing files prior to uploading and shows a simple dialog to choose
+	 * skip all, replace all or choose which files to keep
+	 *
+	 * @param {array} selection of files to upload
+	 * @param {object} callbacks - object with several callback methods
+	 * @param {function} callbacks.onNoConflicts
+	 * @param {function} callbacks.onSkipConflicts
+	 * @param {function} callbacks.onReplaceConflicts
+	 * @param {function} callbacks.onChooseConflicts
+	 * @param {function} callbacks.onCancel
+	 */
+	checkExistingFiles: function (selection, callbacks) {
+		var fileList = this.fileList;
+		var conflicts = [];
+		// only keep non-conflicting uploads
+		selection.uploads = _.filter(selection.uploads, function(upload) {
+			var file = upload.getFile();
+			if (file.relativePath) {
+				// can't check in subfolder contents
+				return true;
+			}
+			if (!fileList) {
+				// no list to check against
+				return true;
+			}
+			var fileInfo = fileList.findFile(file.name);
+			if (fileInfo) {
+				conflicts.push([
+					// original
+					_.extend(fileInfo, {
+						directory: fileInfo.directory || fileInfo.path || fileList.getCurrentDirectory()
+					}),
+					// replacement (File object)
+					upload
+				]);
+				return false;
+			}
+			return true;
+		});
+		if (conflicts.length) {
+			// wait for template loading
+			OC.dialogs.fileexists(null, null, null, this).done(function() {
+				_.each(conflicts, function(conflictData) {
+					OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this);
+				});
+			});
+		}
+
+		// upload non-conflicting files
+		// note: when reaching the server they might still meet conflicts
+		// if the folder was concurrently modified, these will get added
+		// to the already visible dialog, if applicable
+		callbacks.onNoConflicts(selection);
+	},
+
+	_updateProgressBarOnUploadStop: function() {
+		if (this._pendingUploadDoneCount === 0) {
+			// All the uploads ended and there is no pending operation, so hide
+			// the progress bar.
+			// Note that this happens here only with non-chunked uploads; if the
+			// upload was chunked then this will have been executed after all
+			// the uploads ended but before the upload done handler that reduces
+			// the pending operation count was executed.
+			this._hideProgressBar();
+
+			return;
+		}
+
+		this._setProgressBarText(t('files', 'Processing files …'), t('files', '…'));
+
+		// Nothing is being uploaded at this point, and the pending operations
+		// can not be cancelled, so the cancel button should be hidden.
+		this._hideCancelButton();
+	},
+
+	_hideProgressBar: function() {
+		this.progressBar.hideProgressBar();
+	},
+
+	_hideCancelButton: function() {
+		this.progressBar.hideCancelButton();
+	},
+
+	_showProgressBar: function() {
+		this.progressBar.showProgressBar();
+	},
+
+	_setProgressBarValue: function(value) {
+		this.progressBar.setProgressBarValue(value);
+	},
+
+	_setProgressBarText: function(textDesktop, textMobile, title) {
+		this.progressBar.setProgressBarText(textDesktop, textMobile, title);
+	},
+
+	/**
+	 * Returns whether the given file is known to be a received shared file
+	 *
+	 * @param {Object} file file
+	 * @return {bool} true if the file is a shared file
+	 */
+	_isReceivedSharedFile: function(file) {
+		if (!window.FileList) {
+			return false;
+		}
+		var $tr = window.FileList.findFileEl(file.name);
+		if (!$tr.length) {
+			return false;
+		}
+
+		return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
+	},
+
+	/**
+	 * Initialize the upload object
+	 *
+	 * @param {Object} $uploadEl upload element
+	 * @param {Object} options
+	 * @param {OCA.Files.FileList} [options.fileList] file list object
+	 * @param {OC.Files.Client} [options.filesClient] files client object
+	 * @param {Object} [options.dropZone] drop zone for drag and drop upload
+	 */
+	init: function($uploadEl, options) {
+		var self = this;
+
+		options = options || {};
+
+		this.fileList = options.fileList;
+		this.progressBar = options.progressBar;
+		this.filesClient = options.filesClient || OC.Files.getClient();
+		this.davClient = new OC.Files.Client({
+			host: this.filesClient.getHost(),
+			root: OC.linkToRemoteBase('dav'),
+			useHTTPS: OC.getProtocol() === 'https',
+			userName: this.filesClient.getUserName(),
+			password: this.filesClient.getPassword()
+		});
+
+		$uploadEl = $($uploadEl);
+		this.$uploadEl = $uploadEl;
+
+		if ($uploadEl.exists()) {
+			this.progressBar.on('cancel', function() {
+				self.cancelUploads();
+			});
+
+			this.fileUploadParam = {
+				type: 'PUT',
+				dropZone: options.dropZone, // restrict dropZone to content div
+				autoUpload: false,
+				sequentialUploads: false,
+				limitConcurrentUploads: 10,
+				/**
+				 * on first add of every selection
+				 * - check all files of originalFiles array with files in dir
+				 * - on conflict show dialog
+				 *   - skip all -> remember as single skip action for all conflicting files
+				 *   - replace all -> remember as single replace action for all conflicting files
+				 *   - choose -> show choose dialog
+				 *     - mark files to keep
+				 *       - when only existing -> remember as single skip action
+				 *       - when only new -> remember as single replace action
+				 *       - when both -> remember as single autorename action
+				 * - start uploading selection
+				 * @param {object} e
+				 * @param {object} data
+				 * @returns {boolean}
+				 */
+				add: function(e, data) {
+					self.log('add', e, data);
+					var that = $(this), freeSpace;
+
+					var upload = new OC.FileUpload(self, data);
+					// can't link directly due to jQuery not liking cyclic deps on its ajax object
+					data.uploadId = upload.getId();
+
+					// create a container where we can store the data objects
+					if ( ! data.originalFiles.selection ) {
+						// initialize selection and remember number of files to upload
+						data.originalFiles.selection = {
+							uploads: [],
+							filesToUpload: data.originalFiles.length,
+							totalBytes: 0
+						};
+					}
+					// TODO: move originalFiles to a separate container, maybe inside OC.Upload
+					var selection = data.originalFiles.selection;
+
+					// add uploads
+					if ( selection.uploads.length < selection.filesToUpload ) {
+						// remember upload
+						selection.uploads.push(upload);
+					}
+
+					//examine file
+					var file = upload.getFile();
+					try {
+						// FIXME: not so elegant... need to refactor that method to return a value
+						Files.isFileNameValid(file.name);
+					}
+					catch (errorMessage) {
+						data.textStatus = 'invalidcharacters';
+						data.errorThrown = errorMessage;
+					}
+
+					if (data.targetDir) {
+						upload.setTargetFolder(data.targetDir);
+						delete data.targetDir;
+					}
+
+					// in case folder drag and drop is not supported file will point to a directory
+					// http://stackoverflow.com/a/20448357
+					if ( ! file.type && file.size % 4096 === 0 && file.size <= 102400) {
+						var dirUploadFailure = false;
+						try {
+							var reader = new FileReader();
+							reader.readAsBinaryString(file);
+						} catch (NS_ERROR_FILE_ACCESS_DENIED) {
+							//file is a directory
+							dirUploadFailure = true;
+						}
+
+						if (dirUploadFailure) {
+							data.textStatus = 'dirorzero';
+							data.errorThrown = t('files',
+								'Unable to upload {filename} as it is a directory or has 0 bytes',
+								{filename: file.name}
+							);
+						}
+					}
+
+					// only count if we're not overwriting an existing shared file
+					if (self._isReceivedSharedFile(file)) {
+						file.isReceivedShare = true;
+					} else {
+						// add size
+						selection.totalBytes += file.size;
+					}
+
+					// check free space
+					freeSpace = $('#free_space').val();
+					if (freeSpace >= 0 && selection.totalBytes > freeSpace) {
+						data.textStatus = 'notenoughspace';
+						data.errorThrown = t('files',
+							'Not enough free space, you are uploading {size1} but only {size2} is left', {
+							'size1': humanFileSize(selection.totalBytes),
+							'size2': humanFileSize($('#free_space').val())
+						});
+					}
+
+					// end upload for whole selection on error
+					if (data.errorThrown) {
+						// trigger fileupload fail handler
+						var fu = that.data('blueimp-fileupload') || that.data('fileupload');
+						fu._trigger('fail', e, data);
+						return false; //don't upload anything
+					}
+
+					// check existing files when all is collected
+					if ( selection.uploads.length >= selection.filesToUpload ) {
+
+						//remove our selection hack:
+						delete data.originalFiles.selection;
+
+						var callbacks = {
+
+							onNoConflicts: function (selection) {
+								self.submitUploads(selection.uploads);
+							},
+							onSkipConflicts: function (selection) {
+								//TODO mark conflicting files as toskip
+							},
+							onReplaceConflicts: function (selection) {
+								//TODO mark conflicting files as toreplace
+							},
+							onChooseConflicts: function (selection) {
+								//TODO mark conflicting files as chosen
+							},
+							onCancel: function (selection) {
+								$.each(selection.uploads, function(i, upload) {
+									upload.abort();
+								});
+							}
+						};
+
+						self.checkExistingFiles(selection, callbacks);
+
+					}
+
+					return true; // continue adding files
+				},
+				/**
+				 * called after the first add, does NOT have the data param
+				 * @param {object} e
+				 */
+				start: function(e) {
+					self.log('start', e, null);
+					//hide the tooltip otherwise it covers the progress bar
+					$('#upload').tooltip('hide');
+					self._uploading = true;
+				},
+				fail: function(e, data) {
+					var upload = self.getUpload(data);
+					var status = null;
+					if (upload) {
+						status = upload.getResponseStatus();
+					}
+					self.log('fail', e, upload);
+
+					self.removeUpload(upload);
+
+					if (data.textStatus === 'abort' || data.errorThrown === 'abort') {
+						self.showUploadCancelMessage();
+					} else if (status === 412) {
+						// file already exists
+						self.showConflict(upload);
+					} else if (status === 404) {
+						// target folder does not exist any more
+						OC.Notification.show(t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()} ), {type: 'error'});
+						self.cancelUploads();
+					} else if (data.textStatus === 'notenoughspace') {
+						// not enough space
+						OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'});
+						self.cancelUploads();
+					} else {
+						// HTTP connection problem or other error
+						var message = t('files', 'An unknown error has occurred');
+						if (upload) {
+							var response = upload.getResponse();
+							if (response) {
+								message = response.message;
+							}
+						}
+						OC.Notification.show(message || data.errorThrown, {type: 'error'});
+					}
+
+					if (upload) {
+						upload.fail();
+					}
+				},
+				/**
+				 * called for every successful upload
+				 * @param {object} e
+				 * @param {object} data
+				 */
+				done:function(e, data) {
+					var upload = self.getUpload(data);
+					var that = $(this);
+					self.log('done', e, upload);
+
+					self.removeUpload(upload);
+
+					var status = upload.getResponseStatus();
+					if (status < 200 || status >= 300) {
+						// trigger fail handler
+						var fu = that.data('blueimp-fileupload') || that.data('fileupload');
+						fu._trigger('fail', e, data);
+						return;
+					}
+				},
+				/**
+				 * called after last upload
+				 * @param {object} e
+				 * @param {object} data
+				 */
+				stop: function(e, data) {
+					self.log('stop', e, data);
+					self._uploading = false;
+				}
+			};
+
+			if (options.maxChunkSize) {
+				this.fileUploadParam.maxChunkSize = options.maxChunkSize;
+			}
+
+			// initialize jquery fileupload (blueimp)
+			var fileupload = this.$uploadEl.fileupload(this.fileUploadParam);
+
+			if (this._supportAjaxUploadWithProgress()) {
+				//remaining time
+				var lastUpdate, lastSize, bufferSize, buffer, bufferIndex, bufferIndex2, bufferTotal;
+
+				var dragging = false;
+
+				// add progress handlers
+				fileupload.on('fileuploadadd', function(e, data) {
+					self.log('progress handle fileuploadadd', e, data);
+					self.trigger('add', e, data);
+				});
+				// add progress handlers
+				fileupload.on('fileuploadstart', function(e, data) {
+					self.log('progress handle fileuploadstart', e, data);
+					self._setProgressBarText(t('files', 'Uploading …'), t('files', '…'));
+					self._setProgressBarValue(0);
+					self._showProgressBar();
+					// initial remaining time variables
+					lastUpdate   = new Date().getTime();
+					lastSize     = 0;
+					bufferSize   = 20;
+					buffer       = [];
+					bufferIndex  = 0;
+					bufferIndex2 = 0;
+					bufferTotal  = 0;
+					for(var i = 0; i < bufferSize; i++){
+						buffer[i]  = 0;
+					}
+					self.trigger('start', e, data);
+				});
+				fileupload.on('fileuploadprogress', function(e, data) {
+					self.log('progress handle fileuploadprogress', e, data);
+					//TODO progressbar in row
+					self.trigger('progress', e, data);
+				});
+				fileupload.on('fileuploadprogressall', function(e, data) {
+					self.log('progress handle fileuploadprogressall', e, data);
+					var total = self.totalToUpload;
+					var progress = (data.loaded / total) * 100;
+					var thisUpdate = new Date().getTime();
+					var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s
+					lastUpdate = thisUpdate;
+					var diffSize = data.loaded - lastSize;
+					lastSize = data.loaded;
+					diffSize = diffSize / diffUpdate; // apply timing factor, eg. 1MiB/2s = 0.5MiB/s, unit is byte per second
+					var remainingSeconds = ((total - data.loaded) / diffSize);
+					if(remainingSeconds >= 0) {
+						bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
+						buffer[bufferIndex] = remainingSeconds; //buffer to make it smoother
+						bufferIndex = (bufferIndex + 1) % bufferSize;
+						bufferIndex2++;
+					}
+					var smoothRemainingSeconds;
+					if (bufferIndex2 > 0 && bufferIndex2 < 20) {
+						smoothRemainingSeconds = bufferTotal / bufferIndex2;
+					} else if (bufferSize > 0) {
+						smoothRemainingSeconds = bufferTotal / bufferSize;
+					} else {
+						smoothRemainingSeconds = 1;
+					}
+
+					var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
+					if (!(smoothRemainingSeconds >= 0 && smoothRemainingSeconds < 14400)) {
+						// show "Uploading ..." for durations longer than 4 hours
+						h = t('files', 'Uploading …');
+					}
+					self._setProgressBarText(h, h, t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
+							loadedSize: humanFileSize(data.loaded),
+							totalSize: humanFileSize(total),
+							bitrate: humanFileSize(data.bitrate / 8) + '/s'
+						}));
+					self._setProgressBarValue(progress);
+					self.trigger('progressall', e, data);
+				});
+				fileupload.on('fileuploadstop', function(e, data) {
+					self.log('progress handle fileuploadstop', e, data);
+
+					self.clear();
+					self._updateProgressBarOnUploadStop();
+					self.trigger('stop', e, data);
+				});
+				fileupload.on('fileuploadfail', function(e, data) {
+					self.log('progress handle fileuploadfail', e, data);
+					self.trigger('fail', e, data);
+				});
+				fileupload.on('fileuploaddragover', function(e){
+					$('#app-content').addClass('file-drag');
+					$('#emptycontent .icon-folder').addClass('icon-filetype-folder-drag-accept');
+
+					var filerow = $(e.delegatedEvent.target).closest('tr');
+
+					if(!filerow.hasClass('dropping-to-dir')){
+						$('.dropping-to-dir .icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
+						$('.dropping-to-dir').removeClass('dropping-to-dir');
+						$('.dir-drop').removeClass('dir-drop');
+					}
+
+					if(filerow.attr('data-type') === 'dir'){
+						$('#app-content').addClass('dir-drop');
+						filerow.addClass('dropping-to-dir');
+						filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
+					}
+
+					dragging = true;
+				});
+
+				var disableDropState = function() {
+					$('#app-content').removeClass('file-drag');
+					$('.dropping-to-dir').removeClass('dropping-to-dir');
+					$('.dir-drop').removeClass('dir-drop');
+					$('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
+
+					dragging = false;
+				};
+
+				fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState);
+
+				// In some browsers the "drop" event can be triggered with no
+				// files even if the "dragover" event seemed to suggest that a
+				// file was being dragged (and thus caused "fileuploaddragover"
+				// to be triggered).
+				fileupload.on('fileuploaddropnofiles', function() {
+					if (!dragging) {
+						return;
+					}
+
+					disableDropState();
+
+					OC.Notification.show(t('files', 'Uploading that item is not supported'), {type: 'error'});
+				});
+
+				fileupload.on('fileuploadchunksend', function(e, data) {
+					// modify the request to adjust it to our own chunking
+					var upload = self.getUpload(data);
+					var range = data.contentRange.split(' ')[1];
+					var chunkId = range.split('/')[0].split('-')[0];
+					data.url = OC.getRootPath() +
+						'/remote.php/dav/uploads' +
+						'/' + OC.getCurrentUser().uid +
+						'/' + upload.getId() +
+						'/' + chunkId;
+					delete data.contentRange;
+					delete data.headers['Content-Range'];
+				});
+				fileupload.on('fileuploaddone', function(e, data) {
+					var upload = self.getUpload(data);
+
+					self._pendingUploadDoneCount++;
+
+					upload.done().then(function() {
+						self._pendingUploadDoneCount--;
+						if (Object.keys(self._uploads).length === 0 && self._pendingUploadDoneCount === 0) {
+							// All the uploads ended and there is no pending
+							// operation, so hide the progress bar.
+							// Note that this happens here only with chunked
+							// uploads; if the upload was non-chunked then this
+							// handler is immediately executed, before the
+							// jQuery upload done handler that removes the
+							// upload from the list, and thus at this point
+							// there is still at least one upload that has not
+							// ended (although the upload stop handler is always
+							// executed after all the uploads have ended, which
+							// hides the progress bar in that case).
+							self._hideProgressBar();
+						}
+
+						self.trigger('done', e, upload);
+					}).fail(function(status, response) {
+						var message = response.message;
+						if (status === 507) {
+							// not enough space
+							OC.Notification.show(message || t('files', 'Not enough free space'), {type: 'error'});
+							self.cancelUploads();
+						} else if (status === 409) {
+							OC.Notification.show(message || t('files', 'Target folder does not exist any more'), {type: 'error'});
+						} else {
+							OC.Notification.show(message || t('files', 'Error when assembling chunks, status code {status}', {status: status}), {type: 'error'});
+						}
+						self.trigger('fail', e, data);
+					});
+				});
+				fileupload.on('fileuploaddrop', function(e, data) {
+					self.trigger('drop', e, data);
+					if (e.isPropagationStopped()) {
+						return false;
+					}
+				});
+			}
+			window.onbeforeunload = function() {
+				return self.confirmBeforeUnload();
+			}
+		}
+
+		//add multiply file upload attribute to all browsers except konqueror (which crashes when it's used)
+		if (navigator.userAgent.search(/konqueror/i) === -1) {
+			this.$uploadEl.attr('multiple', 'multiple');
+		}
+
+		return this.fileUploadParam;
+	}
+}, OC.Backbone.Events);
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global Files */
+
+(function() {
+
+	/**
+	 * Construct a new NewFileMenu instance
+	 * @constructs NewFileMenu
+	 *
+	 * @memberof OCA.Files
+	 */
+	var NewFileMenu = OC.Backbone.View.extend({
+		tagName: 'div',
+		// Menu is opened by default because it's rendered on "add-button" click
+		className: 'newFileMenu popovermenu bubble menu open menu-left',
+
+		events: {
+			'click .menuitem': '_onClickAction'
+		},
+
+		/**
+		 * @type OCA.Files.FileList
+		 */
+		fileList: null,
+
+		initialize: function(options) {
+			var self = this;
+			var $uploadEl = $('#file_upload_start');
+			if ($uploadEl.length) {
+				$uploadEl.on('fileuploadstart', function() {
+					self.trigger('actionPerformed', 'upload');
+				});
+			} else {
+				console.warn('Missing upload element "file_upload_start"');
+			}
+
+			this.fileList = options && options.fileList;
+
+			this._menuItems = [{
+				id: 'folder',
+				displayName: t('files', 'New folder'),
+				templateName: t('files', 'New folder'),
+				iconClass: 'icon-folder',
+				fileType: 'folder',
+				actionHandler: function(name) {
+					self.fileList.createDirectory(name);
+				}
+		        }];
+
+			OC.Plugins.attach('OCA.Files.NewFileMenu', this);
+		},
+
+		template: function(data) {
+			return OCA.Files.Templates['newfilemenu'](data);
+		},
+
+		/**
+		 * Event handler whenever an action has been clicked within the menu
+		 *
+		 * @param {Object} event event object
+		 */
+		_onClickAction: function(event) {
+			var $target = $(event.target);
+			if (!$target.hasClass('menuitem')) {
+				$target = $target.closest('.menuitem');
+			}
+			var action = $target.attr('data-action');
+			// note: clicking the upload label will automatically
+			// set the focus on the "file_upload_start" hidden field
+			// which itself triggers the upload dialog.
+			// Currently the upload logic is still in file-upload.js and filelist.js
+			if (action === 'upload') {
+				OC.hideMenus();
+			} else {
+				event.preventDefault();
+				this.$el.find('.menuitem.active').removeClass('active');
+				$target.addClass('active');
+				this._promptFileName($target);
+			}
+		},
+
+		_promptFileName: function($target) {
+			var self = this;
+
+			if ($target.find('form').length) {
+				$target.find('input[type=\'text\']').focus();
+				return;
+			}
+
+			// discard other forms
+			this.$el.find('form').remove();
+			this.$el.find('.displayname').removeClass('hidden');
+
+			$target.find('.displayname').addClass('hidden');
+
+			var newName = $target.attr('data-templatename');
+			var fileType = $target.attr('data-filetype');
+			var $form = $(OCA.Files.Templates['newfilemenu_filename_form']({
+				fileName: newName,
+				cid: this.cid,
+				fileType: fileType
+			}));
+
+			//this.trigger('actionPerformed', action);
+			$target.append($form);
+
+			// here comes the OLD code
+			var $input = $form.find('input[type=\'text\']');
+			var $submit = $form.find('input[type=\'submit\']');
+
+			var lastPos;
+			var checkInput = function () {
+				var filename = $input.val();
+				try {
+					if (!Files.isFileNameValid(filename)) {
+						// Files.isFileNameValid(filename) throws an exception itself
+					} else if (self.fileList.inList(filename)) {
+						throw t('files', '{newName} already exists', {newName: filename}, undefined, {
+							escape: false
+						});
+					} else {
+						return true;
+					}
+				} catch (error) {
+					$input.attr('title', error);
+					$input.tooltip({placement: 'right', trigger: 'manual', 'container': '.newFileMenu'});
+					$input.tooltip('fixTitle');
+					$input.tooltip('show');
+					$input.addClass('error');
+				}
+				return false;
+			};
+
+			// verify filename on typing
+			$input.keyup(function() {
+				if (checkInput()) {
+					$input.tooltip('hide');
+					$input.removeClass('error');
+				}
+			});
+
+			$submit.click(function(event) {
+				event.stopPropagation();
+				event.preventDefault();
+				$form.submit();
+			});
+
+			$input.focus();
+			// pre select name up to the extension
+			lastPos = newName.lastIndexOf('.');
+			if (lastPos === -1) {
+				lastPos = newName.length;
+			}
+			$input.selectRange(0, lastPos);
+
+			$form.submit(function(event) {
+				event.stopPropagation();
+				event.preventDefault();
+
+				if (checkInput()) {
+					var newname = $input.val().trim();
+
+					/* Find the right actionHandler that should be called.
+					 * Actions is retrieved by using `actionSpec.id` */
+					var action = _.filter(self._menuItems, function(item) {
+						return item.id == $target.attr('data-action');
+					}).pop();
+					action.actionHandler(newname);
+
+					$form.remove();
+					$target.find('.displayname').removeClass('hidden');
+					OC.hideMenus();
+				}
+			});
+		},
+
+		/**
+		* Add a new item menu entry in the “New” file menu (in
+		* last position). By clicking on the item, the
+		* `actionHandler` function is called.
+		*
+		* @param {Object} actionSpec item’s properties
+		*/
+		addMenuEntry: function(actionSpec) {
+			this._menuItems.push({
+				id: actionSpec.id,
+				displayName: actionSpec.displayName,
+				templateName: actionSpec.templateName,
+				iconClass: actionSpec.iconClass,
+				fileType: actionSpec.fileType,
+				actionHandler: actionSpec.actionHandler,
+		        });
+		},
+
+		/**
+		 * Renders the menu with the currently set items
+		 */
+		render: function() {
+			this.$el.html(this.template({
+				uploadMaxHumanFileSize: 'TODO',
+				uploadLabel: t('files', 'Upload file'),
+				items: this._menuItems
+			}));
+
+			// Trigger upload action also with keyboard navigation on enter
+			this.$el.find('[for="file_upload_start"]').on('keyup', function(event) {
+				if (event.key === " " || event.key === "Enter") {
+					$('#file_upload_start').trigger('click');
+				}
+			});
+		},
+
+		/**
+		 * Displays the menu under the given element
+		 *
+		 * @param {Object} $target target element
+		 */
+		showAt: function($target) {
+			this.render();
+			OC.showMenu(null, this.$el);
+		}
+	});
+
+	OCA.Files.NewFileMenu = NewFileMenu;
+
+})();
+
+
+/*
+ * jQuery File Upload Plugin 9.12.5
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/* jshint nomen:false */
+/* global define, require, window, document, location, Blob, FormData */
+
+;(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define([
+            'jquery',
+            'jquery.ui.widget'
+        ], factory);
+    } else if (typeof exports === 'object') {
+        // Node/CommonJS:
+        factory(
+            require('jquery'),
+            require('./vendor/jquery.ui.widget')
+        );
+    } else {
+        // Browser globals:
+        factory(window.jQuery);
+    }
+}(function ($) {
+    'use strict';
+
+    // Detect file input support, based on
+    // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
+    $.support.fileInput = !(new RegExp(
+        // Handle devices which give false positives for the feature detection:
+        '(Android (1\\.[0156]|2\\.[01]))' +
+            '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
+            '|(w(eb)?OSBrowser)|(webOS)' +
+            '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
+    ).test(window.navigator.userAgent) ||
+        // Feature detection for all other devices:
+        $('<input type="file">').prop('disabled'));
+
+    // The FileReader API is not actually used, but works as feature detection,
+    // as some Safari versions (5?) support XHR file uploads via the FormData API,
+    // but not non-multipart XHR file uploads.
+    // window.XMLHttpRequestUpload is not available on IE10, so we check for
+    // window.ProgressEvent instead to detect XHR2 file upload capability:
+    $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
+    $.support.xhrFormDataFileUpload = !!window.FormData;
+
+    // Detect support for Blob slicing (required for chunked uploads):
+    $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
+        Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
+
+    // Helper function to create drag handlers for dragover/dragenter/dragleave:
+    function getDragHandler(type) {
+        var isDragOver = type === 'dragover';
+        return function (e) {
+            e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
+            var dataTransfer = e.dataTransfer;
+            if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
+                    this._trigger(
+                        type,
+                        $.Event(type, {delegatedEvent: e})
+                    ) !== false) {
+                e.preventDefault();
+                if (isDragOver) {
+                    dataTransfer.dropEffect = 'copy';
+                }
+            }
+        };
+    }
+
+    // The fileupload widget listens for change events on file input fields defined
+    // via fileInput setting and paste or drop events of the given dropZone.
+    // In addition to the default jQuery Widget methods, the fileupload widget
+    // exposes the "add" and "send" methods, to add or directly send files using
+    // the fileupload API.
+    // By default, files added via file input selection, paste, drag & drop or
+    // "add" method are uploaded immediately, but it is possible to override
+    // the "add" callback option to queue file uploads.
+    $.widget('blueimp.fileupload', {
+
+        options: {
+            // The drop target element(s), by the default the complete document.
+            // Set to null to disable drag & drop support:
+            dropZone: $(document),
+            // The paste target element(s), by the default undefined.
+            // Set to a DOM node or jQuery object to enable file pasting:
+            pasteZone: undefined,
+            // The file input field(s), that are listened to for change events.
+            // If undefined, it is set to the file input fields inside
+            // of the widget element on plugin initialization.
+            // Set to null to disable the change listener.
+            fileInput: undefined,
+            // By default, the file input field is replaced with a clone after
+            // each input field change event. This is required for iframe transport
+            // queues and allows change events to be fired for the same file
+            // selection, but can be disabled by setting the following option to false:
+            replaceFileInput: true,
+            // The parameter name for the file form data (the request argument name).
+            // If undefined or empty, the name property of the file input field is
+            // used, or "files[]" if the file input name property is also empty,
+            // can be a string or an array of strings:
+            paramName: undefined,
+            // By default, each file of a selection is uploaded using an individual
+            // request for XHR type uploads. Set to false to upload file
+            // selections in one request each:
+            singleFileUploads: true,
+            // To limit the number of files uploaded with one XHR request,
+            // set the following option to an integer greater than 0:
+            limitMultiFileUploads: undefined,
+            // The following option limits the number of files uploaded with one
+            // XHR request to keep the request size under or equal to the defined
+            // limit in bytes:
+            limitMultiFileUploadSize: undefined,
+            // Multipart file uploads add a number of bytes to each uploaded file,
+            // therefore the following option adds an overhead for each file used
+            // in the limitMultiFileUploadSize configuration:
+            limitMultiFileUploadSizeOverhead: 512,
+            // Set the following option to true to issue all file upload requests
+            // in a sequential order:
+            sequentialUploads: false,
+            // To limit the number of concurrent uploads,
+            // set the following option to an integer greater than 0:
+            limitConcurrentUploads: undefined,
+            // Set the following option to true to force iframe transport uploads:
+            forceIframeTransport: false,
+            // Set the following option to the location of a redirect url on the
+            // origin server, for cross-domain iframe transport uploads:
+            redirect: undefined,
+            // The parameter name for the redirect url, sent as part of the form
+            // data and set to 'redirect' if this option is empty:
+            redirectParamName: undefined,
+            // Set the following option to the location of a postMessage window,
+            // to enable postMessage transport uploads:
+            postMessage: undefined,
+            // By default, XHR file uploads are sent as multipart/form-data.
+            // The iframe transport is always using multipart/form-data.
+            // Set to false to enable non-multipart XHR uploads:
+            multipart: true,
+            // To upload large files in smaller chunks, set the following option
+            // to a preferred maximum chunk size. If set to 0, null or undefined,
+            // or the browser does not support the required Blob API, files will
+            // be uploaded as a whole.
+            maxChunkSize: undefined,
+            // When a non-multipart upload or a chunked multipart upload has been
+            // aborted, this option can be used to resume the upload by setting
+            // it to the size of the already uploaded bytes. This option is most
+            // useful when modifying the options object inside of the "add" or
+            // "send" callbacks, as the options are cloned for each file upload.
+            uploadedBytes: undefined,
+            // By default, failed (abort or error) file uploads are removed from the
+            // global progress calculation. Set the following option to false to
+            // prevent recalculating the global progress data:
+            recalculateProgress: true,
+            // Interval in milliseconds to calculate and trigger progress events:
+            progressInterval: 100,
+            // Interval in milliseconds to calculate progress bitrate:
+            bitrateInterval: 500,
+            // By default, uploads are started automatically when adding files:
+            autoUpload: true,
+
+            // Error and info messages:
+            messages: {
+                uploadedBytes: 'Uploaded bytes exceed file size'
+            },
+
+            // Translation function, gets the message key to be translated
+            // and an object with context specific data as arguments:
+            i18n: function (message, context) {
+                message = this.messages[message] || message.toString();
+                if (context) {
+                    $.each(context, function (key, value) {
+                        message = message.replace('{' + key + '}', value);
+                    });
+                }
+                return message;
+            },
+
+            // Additional form data to be sent along with the file uploads can be set
+            // using this option, which accepts an array of objects with name and
+            // value properties, a function returning such an array, a FormData
+            // object (for XHR file uploads), or a simple object.
+            // The form of the first fileInput is given as parameter to the function:
+            formData: function (form) {
+                return form.serializeArray();
+            },
+
+            // The add callback is invoked as soon as files are added to the fileupload
+            // widget (via file input selection, drag & drop, paste or add API call).
+            // If the singleFileUploads option is enabled, this callback will be
+            // called once for each file in the selection for XHR file uploads, else
+            // once for each file selection.
+            //
+            // The upload starts when the submit method is invoked on the data parameter.
+            // The data object contains a files property holding the added files
+            // and allows you to override plugin options as well as define ajax settings.
+            //
+            // Listeners for this callback can also be bound the following way:
+            // .bind('fileuploadadd', func);
+            //
+            // data.submit() returns a Promise object and allows to attach additional
+            // handlers using jQuery's Deferred callbacks:
+            // data.submit().done(func).fail(func).always(func);
+            add: function (e, data) {
+                if (e.isDefaultPrevented()) {
+                    return false;
+                }
+                if (data.autoUpload || (data.autoUpload !== false &&
+                        $(this).fileupload('option', 'autoUpload'))) {
+                    data.process().done(function () {
+                        data.submit();
+                    });
+                }
+            },
+
+            // Other callbacks:
+
+            // Callback for the submit event of each file upload:
+            // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
+
+            // Callback for the start of each file upload request:
+            // send: function (e, data) {}, // .bind('fileuploadsend', func);
+
+            // Callback for successful uploads:
+            // done: function (e, data) {}, // .bind('fileuploaddone', func);
+
+            // Callback for failed (abort or error) uploads:
+            // fail: function (e, data) {}, // .bind('fileuploadfail', func);
+
+            // Callback for completed (success, abort or error) requests:
+            // always: function (e, data) {}, // .bind('fileuploadalways', func);
+
+            // Callback for upload progress events:
+            // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
+
+            // Callback for global upload progress events:
+            // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
+
+            // Callback for uploads start, equivalent to the global ajaxStart event:
+            // start: function (e) {}, // .bind('fileuploadstart', func);
+
+            // Callback for uploads stop, equivalent to the global ajaxStop event:
+            // stop: function (e) {}, // .bind('fileuploadstop', func);
+
+            // Callback for change events of the fileInput(s):
+            // change: function (e, data) {}, // .bind('fileuploadchange', func);
+
+            // Callback for paste events to the pasteZone(s):
+            // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
+
+            // Callback for drop events of the dropZone(s):
+            // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
+
+            // Callback for drop events of the dropZone(s) when there are no files:
+            // dropnofiles: function (e) {}, // .bind('fileuploaddropnofiles', func);
+
+            // Callback for dragover events of the dropZone(s):
+            // dragover: function (e) {}, // .bind('fileuploaddragover', func);
+
+            // Callback for the start of each chunk upload request:
+            // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
+
+            // Callback for successful chunk uploads:
+            // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
+
+            // Callback for failed (abort or error) chunk uploads:
+            // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
+
+            // Callback for completed (success, abort or error) chunk upload requests:
+            // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
+
+            // The plugin options are used as settings object for the ajax calls.
+            // The following are jQuery ajax settings required for the file uploads:
+            processData: false,
+            contentType: false,
+            cache: false,
+            timeout: 0
+        },
+
+        // A list of options that require reinitializing event listeners and/or
+        // special initialization code:
+        _specialOptions: [
+            'fileInput',
+            'dropZone',
+            'pasteZone',
+            'multipart',
+            'forceIframeTransport'
+        ],
+
+        _blobSlice: $.support.blobSlice && function () {
+            var slice = this.slice || this.webkitSlice || this.mozSlice;
+            return slice.apply(this, arguments);
+        },
+
+        _BitrateTimer: function () {
+            this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
+            this.loaded = 0;
+            this.bitrate = 0;
+            this.getBitrate = function (now, loaded, interval) {
+                var timeDiff = now - this.timestamp;
+                if (!this.bitrate || !interval || timeDiff > interval) {
+                    this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
+                    this.loaded = loaded;
+                    this.timestamp = now;
+                }
+                return this.bitrate;
+            };
+        },
+
+        _isXHRUpload: function (options) {
+            return !options.forceIframeTransport &&
+                ((!options.multipart && $.support.xhrFileUpload) ||
+                $.support.xhrFormDataFileUpload);
+        },
+
+        _getFormData: function (options) {
+            var formData;
+            if ($.type(options.formData) === 'function') {
+                return options.formData(options.form);
+            }
+            if ($.isArray(options.formData)) {
+                return options.formData;
+            }
+            if ($.type(options.formData) === 'object') {
+                formData = [];
+                $.each(options.formData, function (name, value) {
+                    formData.push({name: name, value: value});
+                });
+                return formData;
+            }
+            return [];
+        },
+
+        _getTotal: function (files) {
+            var total = 0;
+            $.each(files, function (index, file) {
+                total += file.size || 1;
+            });
+            return total;
+        },
+
+        _initProgressObject: function (obj) {
+            var progress = {
+                loaded: 0,
+                total: 0,
+                bitrate: 0
+            };
+            if (obj._progress) {
+                $.extend(obj._progress, progress);
+            } else {
+                obj._progress = progress;
+            }
+        },
+
+        _initResponseObject: function (obj) {
+            var prop;
+            if (obj._response) {
+                for (prop in obj._response) {
+                    if (obj._response.hasOwnProperty(prop)) {
+                        delete obj._response[prop];
+                    }
+                }
+            } else {
+                obj._response = {};
+            }
+        },
+
+        _onProgress: function (e, data) {
+            if (e.lengthComputable) {
+                var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
+                    loaded;
+                if (data._time && data.progressInterval &&
+                        (now - data._time < data.progressInterval) &&
+                        e.loaded !== e.total) {
+                    return;
+                }
+                data._time = now;
+                loaded = Math.floor(
+                    e.loaded / e.total * (data.chunkSize || data._progress.total)
+                ) + (data.uploadedBytes || 0);
+                // Add the difference from the previously loaded state
+                // to the global loaded counter:
+                this._progress.loaded += (loaded - data._progress.loaded);
+                this._progress.bitrate = this._bitrateTimer.getBitrate(
+                    now,
+                    this._progress.loaded,
+                    data.bitrateInterval
+                );
+                data._progress.loaded = data.loaded = loaded;
+                data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
+                    now,
+                    loaded,
+                    data.bitrateInterval
+                );
+                // Trigger a custom progress event with a total data property set
+                // to the file size(s) of the current upload and a loaded data
+                // property calculated accordingly:
+                this._trigger(
+                    'progress',
+                    $.Event('progress', {delegatedEvent: e}),
+                    data
+                );
+                // Trigger a global progress event for all current file uploads,
+                // including ajax calls queued for sequential file uploads:
+                this._trigger(
+                    'progressall',
+                    $.Event('progressall', {delegatedEvent: e}),
+                    this._progress
+                );
+            }
+        },
+
+        _initProgressListener: function (options) {
+            var that = this,
+                xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
+            // Accesss to the native XHR object is required to add event listeners
+            // for the upload progress event:
+            if (xhr.upload) {
+                $(xhr.upload).bind('progress', function (e) {
+                    var oe = e.originalEvent;
+                    // Make sure the progress event properties get copied over:
+                    e.lengthComputable = oe.lengthComputable;
+                    e.loaded = oe.loaded;
+                    e.total = oe.total;
+                    that._onProgress(e, options);
+                });
+                options.xhr = function () {
+                    return xhr;
+                };
+            }
+        },
+
+        _isInstanceOf: function (type, obj) {
+            // Cross-frame instanceof check
+            return Object.prototype.toString.call(obj) === '[object ' + type + ']';
+        },
+
+        _initXHRData: function (options) {
+            var that = this,
+                formData,
+                file = options.files[0],
+                // Ignore non-multipart setting if not supported:
+                multipart = options.multipart || !$.support.xhrFileUpload,
+                paramName = $.type(options.paramName) === 'array' ?
+                    options.paramName[0] : options.paramName;
+            options.headers = $.extend({}, options.headers);
+            if (options.contentRange) {
+                options.headers['Content-Range'] = options.contentRange;
+            }
+            if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
+                options.headers['Content-Disposition'] = 'attachment; filename="' +
+                    encodeURI(file.name) + '"';
+            }
+            if (!multipart) {
+                options.contentType = file.type || 'application/octet-stream';
+                options.data = options.blob || file;
+            } else if ($.support.xhrFormDataFileUpload) {
+                if (options.postMessage) {
+                    // window.postMessage does not allow sending FormData
+                    // objects, so we just add the File/Blob objects to
+                    // the formData array and let the postMessage window
+                    // create the FormData object out of this array:
+                    formData = this._getFormData(options);
+                    if (options.blob) {
+                        formData.push({
+                            name: paramName,
+                            value: options.blob
+                        });
+                    } else {
+                        $.each(options.files, function (index, file) {
+                            formData.push({
+                                name: ($.type(options.paramName) === 'array' &&
+                                    options.paramName[index]) || paramName,
+                                value: file
+                            });
+                        });
+                    }
+                } else {
+                    if (that._isInstanceOf('FormData', options.formData)) {
+                        formData = options.formData;
+                    } else {
+                        formData = new FormData();
+                        $.each(this._getFormData(options), function (index, field) {
+                            formData.append(field.name, field.value);
+                        });
+                    }
+                    if (options.blob) {
+                        formData.append(paramName, options.blob, file.name);
+                    } else {
+                        $.each(options.files, function (index, file) {
+                            // This check allows the tests to run with
+                            // dummy objects:
+                            if (that._isInstanceOf('File', file) ||
+                                    that._isInstanceOf('Blob', file)) {
+                                formData.append(
+                                    ($.type(options.paramName) === 'array' &&
+                                        options.paramName[index]) || paramName,
+                                    file,
+                                    file.uploadName || file.name
+                                );
+                            }
+                        });
+                    }
+                }
+                options.data = formData;
+            }
+            // Blob reference is not needed anymore, free memory:
+            options.blob = null;
+        },
+
+        _initIframeSettings: function (options) {
+            var targetHost = $('<a></a>').prop('href', options.url).prop('host');
+            // Setting the dataType to iframe enables the iframe transport:
+            options.dataType = 'iframe ' + (options.dataType || '');
+            // The iframe transport accepts a serialized array as form data:
+            options.formData = this._getFormData(options);
+            // Add redirect url to form data on cross-domain uploads:
+            if (options.redirect && targetHost && targetHost !== location.host) {
+                options.formData.push({
+                    name: options.redirectParamName || 'redirect',
+                    value: options.redirect
+                });
+            }
+        },
+
+        _initDataSettings: function (options) {
+            if (this._isXHRUpload(options)) {
+                if (!this._chunkedUpload(options, true)) {
+                    if (!options.data) {
+                        this._initXHRData(options);
+                    }
+                    this._initProgressListener(options);
+                }
+                if (options.postMessage) {
+                    // Setting the dataType to postmessage enables the
+                    // postMessage transport:
+                    options.dataType = 'postmessage ' + (options.dataType || '');
+                }
+            } else {
+                this._initIframeSettings(options);
+            }
+        },
+
+        _getParamName: function (options) {
+            var fileInput = $(options.fileInput),
+                paramName = options.paramName;
+            if (!paramName) {
+                paramName = [];
+                fileInput.each(function () {
+                    var input = $(this),
+                        name = input.prop('name') || 'files[]',
+                        i = (input.prop('files') || [1]).length;
+                    while (i) {
+                        paramName.push(name);
+                        i -= 1;
+                    }
+                });
+                if (!paramName.length) {
+                    paramName = [fileInput.prop('name') || 'files[]'];
+                }
+            } else if (!$.isArray(paramName)) {
+                paramName = [paramName];
+            }
+            return paramName;
+        },
+
+        _initFormSettings: function (options) {
+            // Retrieve missing options from the input field and the
+            // associated form, if available:
+            if (!options.form || !options.form.length) {
+                options.form = $(options.fileInput.prop('form'));
+                // If the given file input doesn't have an associated form,
+                // use the default widget file input's form:
+                if (!options.form.length) {
+                    options.form = $(this.options.fileInput.prop('form'));
+                }
+            }
+            options.paramName = this._getParamName(options);
+            if (!options.url) {
+                options.url = options.form.prop('action') || location.href;
+            }
+            // The HTTP request method must be "POST" or "PUT":
+            options.type = (options.type ||
+                ($.type(options.form.prop('method')) === 'string' &&
+                    options.form.prop('method')) || ''
+                ).toUpperCase();
+            if (options.type !== 'POST' && options.type !== 'PUT' &&
+                    options.type !== 'PATCH') {
+                options.type = 'POST';
+            }
+            if (!options.formAcceptCharset) {
+                options.formAcceptCharset = options.form.attr('accept-charset');
+            }
+        },
+
+        _getAJAXSettings: function (data) {
+            var options = $.extend({}, this.options, data);
+            this._initFormSettings(options);
+            this._initDataSettings(options);
+            return options;
+        },
+
+        // jQuery 1.6 doesn't provide .state(),
+        // while jQuery 1.8+ removed .isRejected() and .isResolved():
+        _getDeferredState: function (deferred) {
+            if (deferred.state) {
+                return deferred.state();
+            }
+            if (deferred.isResolved()) {
+                return 'resolved';
+            }
+            if (deferred.isRejected()) {
+                return 'rejected';
+            }
+            return 'pending';
+        },
+
+        // Maps jqXHR callbacks to the equivalent
+        // methods of the given Promise object:
+        _enhancePromise: function (promise) {
+            promise.success = promise.done;
+            promise.error = promise.fail;
+            promise.complete = promise.always;
+            return promise;
+        },
+
+        // Creates and returns a Promise object enhanced with
+        // the jqXHR methods abort, success, error and complete:
+        _getXHRPromise: function (resolveOrReject, context, args) {
+            var dfd = $.Deferred(),
+                promise = dfd.promise();
+            context = context || this.options.context || promise;
+            if (resolveOrReject === true) {
+                dfd.resolveWith(context, args);
+            } else if (resolveOrReject === false) {
+                dfd.rejectWith(context, args);
+            }
+            promise.abort = dfd.promise;
+            return this._enhancePromise(promise);
+        },
+
+        // Adds convenience methods to the data callback argument:
+        _addConvenienceMethods: function (e, data) {
+            var that = this,
+                getPromise = function (args) {
+                    return $.Deferred().resolveWith(that, args).promise();
+                };
+            data.process = function (resolveFunc, rejectFunc) {
+                if (resolveFunc || rejectFunc) {
+                    data._processQueue = this._processQueue =
+                        (this._processQueue || getPromise([this])).then(
+                            function () {
+                                if (data.errorThrown) {
+                                    return $.Deferred()
+                                        .rejectWith(that, [data]).promise();
+                                }
+                                return getPromise(arguments);
+                            }
+                        ).then(resolveFunc, rejectFunc);
+                }
+                return this._processQueue || getPromise([this]);
+            };
+            data.submit = function () {
+                if (this.state() !== 'pending') {
+                    data.jqXHR = this.jqXHR =
+                        (that._trigger(
+                            'submit',
+                            $.Event('submit', {delegatedEvent: e}),
+                            this
+                        ) !== false) && that._onSend(e, this);
+                }
+                return this.jqXHR || that._getXHRPromise();
+            };
+            data.abort = function () {
+                if (this.jqXHR) {
+                    return this.jqXHR.abort();
+                }
+                this.errorThrown = 'abort';
+                that._trigger('fail', null, this);
+                return that._getXHRPromise(false);
+            };
+            data.state = function () {
+                if (this.jqXHR) {
+                    return that._getDeferredState(this.jqXHR);
+                }
+                if (this._processQueue) {
+                    return that._getDeferredState(this._processQueue);
+                }
+            };
+            data.processing = function () {
+                return !this.jqXHR && this._processQueue && that
+                    ._getDeferredState(this._processQueue) === 'pending';
+            };
+            data.progress = function () {
+                return this._progress;
+            };
+            data.response = function () {
+                return this._response;
+            };
+        },
+
+        // Parses the Range header from the server response
+        // and returns the uploaded bytes:
+        _getUploadedBytes: function (jqXHR) {
+            var range = jqXHR.getResponseHeader('Range'),
+                parts = range && range.split('-'),
+                upperBytesPos = parts && parts.length > 1 &&
+                    parseInt(parts[1], 10);
+            return upperBytesPos && upperBytesPos + 1;
+        },
+
+        // Uploads a file in multiple, sequential requests
+        // by splitting the file up in multiple blob chunks.
+        // If the second parameter is true, only tests if the file
+        // should be uploaded in chunks, but does not invoke any
+        // upload requests:
+        _chunkedUpload: function (options, testOnly) {
+            options.uploadedBytes = options.uploadedBytes || 0;
+            var that = this,
+                file = options.files[0],
+                fs = file.size,
+                ub = options.uploadedBytes,
+                mcs = options.maxChunkSize || fs,
+                slice = this._blobSlice,
+                dfd = $.Deferred(),
+                promise = dfd.promise(),
+                jqXHR,
+                upload;
+            if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
+                    options.data) {
+                return false;
+            }
+            if (testOnly) {
+                return true;
+            }
+            if (ub >= fs) {
+                file.error = options.i18n('uploadedBytes');
+                return this._getXHRPromise(
+                    false,
+                    options.context,
+                    [null, 'error', file.error]
+                );
+            }
+            // The chunk upload method:
+            upload = function () {
+                // Clone the options object for each chunk upload:
+                var o = $.extend({}, options),
+                    currentLoaded = o._progress.loaded;
+                o.blob = slice.call(
+                    file,
+                    ub,
+                    ub + mcs,
+                    file.type
+                );
+                // Store the current chunk size, as the blob itself
+                // will be dereferenced after data processing:
+                o.chunkSize = o.blob.size;
+                // Expose the chunk bytes position range:
+                o.contentRange = 'bytes ' + ub + '-' +
+                    (ub + o.chunkSize - 1) + '/' + fs;
+                // Process the upload data (the blob and potential form data):
+                that._initXHRData(o);
+                // Add progress listeners for this chunk upload:
+                that._initProgressListener(o);
+                jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
+                        that._getXHRPromise(false, o.context))
+                    .done(function (result, textStatus, jqXHR) {
+                        ub = that._getUploadedBytes(jqXHR) ||
+                            (ub + o.chunkSize);
+                        // Create a progress event if no final progress event
+                        // with loaded equaling total has been triggered
+                        // for this chunk:
+                        if (currentLoaded + o.chunkSize - o._progress.loaded) {
+                            that._onProgress($.Event('progress', {
+                                lengthComputable: true,
+                                loaded: ub - o.uploadedBytes,
+                                total: ub - o.uploadedBytes
+                            }), o);
+                        }
+                        options.uploadedBytes = o.uploadedBytes = ub;
+                        o.result = result;
+                        o.textStatus = textStatus;
+                        o.jqXHR = jqXHR;
+                        that._trigger('chunkdone', null, o);
+                        that._trigger('chunkalways', null, o);
+                        if (ub < fs) {
+                            // File upload not yet complete,
+                            // continue with the next chunk:
+                            upload();
+                        } else {
+                            dfd.resolveWith(
+                                o.context,
+                                [result, textStatus, jqXHR]
+                            );
+                        }
+                    })
+                    .fail(function (jqXHR, textStatus, errorThrown) {
+                        o.jqXHR = jqXHR;
+                        o.textStatus = textStatus;
+                        o.errorThrown = errorThrown;
+                        that._trigger('chunkfail', null, o);
+                        that._trigger('chunkalways', null, o);
+                        dfd.rejectWith(
+                            o.context,
+                            [jqXHR, textStatus, errorThrown]
+                        );
+                    });
+            };
+            this._enhancePromise(promise);
+            promise.abort = function () {
+                return jqXHR.abort();
+            };
+            upload();
+            return promise;
+        },
+
+        _beforeSend: function (e, data) {
+            if (this._active === 0) {
+                // the start callback is triggered when an upload starts
+                // and no other uploads are currently running,
+                // equivalent to the global ajaxStart event:
+                this._trigger('start');
+                // Set timer for global bitrate progress calculation:
+                this._bitrateTimer = new this._BitrateTimer();
+                // Reset the global progress values:
+                this._progress.loaded = this._progress.total = 0;
+                this._progress.bitrate = 0;
+            }
+            // Make sure the container objects for the .response() and
+            // .progress() methods on the data object are available
+            // and reset to their initial state:
+            this._initResponseObject(data);
+            this._initProgressObject(data);
+            data._progress.loaded = data.loaded = data.uploadedBytes || 0;
+            data._progress.total = data.total = this._getTotal(data.files) || 1;
+            data._progress.bitrate = data.bitrate = 0;
+            this._active += 1;
+            // Initialize the global progress values:
+            this._progress.loaded += data.loaded;
+            this._progress.total += data.total;
+        },
+
+        _onDone: function (result, textStatus, jqXHR, options) {
+            var total = options._progress.total,
+                response = options._response;
+            if (options._progress.loaded < total) {
+                // Create a progress event if no final progress event
+                // with loaded equaling total has been triggered:
+                this._onProgress($.Event('progress', {
+                    lengthComputable: true,
+                    loaded: total,
+                    total: total
+                }), options);
+            }
+            response.result = options.result = result;
+            response.textStatus = options.textStatus = textStatus;
+            response.jqXHR = options.jqXHR = jqXHR;
+            this._trigger('done', null, options);
+        },
+
+        _onFail: function (jqXHR, textStatus, errorThrown, options) {
+            var response = options._response;
+            if (options.recalculateProgress) {
+                // Remove the failed (error or abort) file upload from
+                // the global progress calculation:
+                this._progress.loaded -= options._progress.loaded;
+                this._progress.total -= options._progress.total;
+            }
+            response.jqXHR = options.jqXHR = jqXHR;
+            response.textStatus = options.textStatus = textStatus;
+            response.errorThrown = options.errorThrown = errorThrown;
+            this._trigger('fail', null, options);
+        },
+
+        _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
+            // jqXHRorResult, textStatus and jqXHRorError are added to the
+            // options object via done and fail callbacks
+            this._trigger('always', null, options);
+        },
+
+        _onSend: function (e, data) {
+            if (!data.submit) {
+                this._addConvenienceMethods(e, data);
+            }
+            var that = this,
+                jqXHR,
+                aborted,
+                slot,
+                pipe,
+                options = that._getAJAXSettings(data),
+                send = function () {
+                    that._sending += 1;
+                    // Set timer for bitrate progress calculation:
+                    options._bitrateTimer = new that._BitrateTimer();
+                    jqXHR = jqXHR || (
+                        ((aborted || that._trigger(
+                            'send',
+                            $.Event('send', {delegatedEvent: e}),
+                            options
+                        ) === false) &&
+                        that._getXHRPromise(false, options.context, aborted)) ||
+                        that._chunkedUpload(options) || $.ajax(options)
+                    ).done(function (result, textStatus, jqXHR) {
+                        that._onDone(result, textStatus, jqXHR, options);
+                    }).fail(function (jqXHR, textStatus, errorThrown) {
+                        that._onFail(jqXHR, textStatus, errorThrown, options);
+                    }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
+                        that._onAlways(
+                            jqXHRorResult,
+                            textStatus,
+                            jqXHRorError,
+                            options
+                        );
+                        that._sending -= 1;
+                        that._active -= 1;
+                        if (options.limitConcurrentUploads &&
+                                options.limitConcurrentUploads > that._sending) {
+                            // Start the next queued upload,
+                            // that has not been aborted:
+                            var nextSlot = that._slots.shift();
+                            while (nextSlot) {
+                                if (that._getDeferredState(nextSlot) === 'pending') {
+                                    nextSlot.resolve();
+                                    break;
+                                }
+                                nextSlot = that._slots.shift();
+                            }
+                        }
+                        if (that._active === 0) {
+                            // The stop callback is triggered when all uploads have
+                            // been completed, equivalent to the global ajaxStop event:
+                            that._trigger('stop');
+                        }
+                    });
+                    return jqXHR;
+                };
+            this._beforeSend(e, options);
+            if (this.options.sequentialUploads ||
+                    (this.options.limitConcurrentUploads &&
+                    this.options.limitConcurrentUploads <= this._sending)) {
+                if (this.options.limitConcurrentUploads > 1) {
+                    slot = $.Deferred();
+                    this._slots.push(slot);
+                    pipe = slot.then(send);
+                } else {
+                    this._sequence = this._sequence.then(send, send);
+                    pipe = this._sequence;
+                }
+                // Return the piped Promise object, enhanced with an abort method,
+                // which is delegated to the jqXHR object of the current upload,
+                // and jqXHR callbacks mapped to the equivalent Promise methods:
+                pipe.abort = function () {
+                    aborted = [undefined, 'abort', 'abort'];
+                    if (!jqXHR) {
+                        if (slot) {
+                            slot.rejectWith(options.context, aborted);
+                        }
+                        return send();
+                    }
+                    return jqXHR.abort();
+                };
+                return this._enhancePromise(pipe);
+            }
+            return send();
+        },
+
+        _onAdd: function (e, data) {
+            var that = this,
+                result = true,
+                options = $.extend({}, this.options, data),
+                files = data.files,
+                filesLength = files.length,
+                limit = options.limitMultiFileUploads,
+                limitSize = options.limitMultiFileUploadSize,
+                overhead = options.limitMultiFileUploadSizeOverhead,
+                batchSize = 0,
+                paramName = this._getParamName(options),
+                paramNameSet,
+                paramNameSlice,
+                fileSet,
+                i,
+                j = 0;
+            if (!filesLength) {
+                return false;
+            }
+            if (limitSize && files[0].size === undefined) {
+                limitSize = undefined;
+            }
+            if (!(options.singleFileUploads || limit || limitSize) ||
+                    !this._isXHRUpload(options)) {
+                fileSet = [files];
+                paramNameSet = [paramName];
+            } else if (!(options.singleFileUploads || limitSize) && limit) {
+                fileSet = [];
+                paramNameSet = [];
+                for (i = 0; i < filesLength; i += limit) {
+                    fileSet.push(files.slice(i, i + limit));
+                    paramNameSlice = paramName.slice(i, i + limit);
+                    if (!paramNameSlice.length) {
+                        paramNameSlice = paramName;
+                    }
+                    paramNameSet.push(paramNameSlice);
+                }
+            } else if (!options.singleFileUploads && limitSize) {
+                fileSet = [];
+                paramNameSet = [];
+                for (i = 0; i < filesLength; i = i + 1) {
+                    batchSize += files[i].size + overhead;
+                    if (i + 1 === filesLength ||
+                            ((batchSize + files[i + 1].size + overhead) > limitSize) ||
+                            (limit && i + 1 - j >= limit)) {
+                        fileSet.push(files.slice(j, i + 1));
+                        paramNameSlice = paramName.slice(j, i + 1);
+                        if (!paramNameSlice.length) {
+                            paramNameSlice = paramName;
+                        }
+                        paramNameSet.push(paramNameSlice);
+                        j = i + 1;
+                        batchSize = 0;
+                    }
+                }
+            } else {
+                paramNameSet = paramName;
+            }
+            data.originalFiles = files;
+            $.each(fileSet || files, function (index, element) {
+                var newData = $.extend({}, data);
+                newData.files = fileSet ? element : [element];
+                newData.paramName = paramNameSet[index];
+                that._initResponseObject(newData);
+                that._initProgressObject(newData);
+                that._addConvenienceMethods(e, newData);
+                result = that._trigger(
+                    'add',
+                    $.Event('add', {delegatedEvent: e}),
+                    newData
+                );
+                return result;
+            });
+            return result;
+        },
+
+        _replaceFileInput: function (data) {
+            var input = data.fileInput,
+                inputClone = input.clone(true),
+                restoreFocus = input.is(document.activeElement);
+            // Add a reference for the new cloned file input to the data argument:
+            data.fileInputClone = inputClone;
+            $('<form></form>').append(inputClone)[0].reset();
+            // Detaching allows to insert the fileInput on another form
+            // without loosing the file input value:
+            input.after(inputClone).detach();
+            // If the fileInput had focus before it was detached,
+            // restore focus to the inputClone.
+            if (restoreFocus) {
+                inputClone.focus();
+            }
+            // Avoid memory leaks with the detached file input:
+            $.cleanData(input.unbind('remove'));
+            // Replace the original file input element in the fileInput
+            // elements set with the clone, which has been copied including
+            // event handlers:
+            this.options.fileInput = this.options.fileInput.map(function (i, el) {
+                if (el === input[0]) {
+                    return inputClone[0];
+                }
+                return el;
+            });
+            // If the widget has been initialized on the file input itself,
+            // override this.element with the file input clone:
+            if (input[0] === this.element[0]) {
+                this.element = inputClone;
+            }
+        },
+
+        _handleFileTreeEntry: function (entry, path) {
+            var that = this,
+                dfd = $.Deferred(),
+                errorHandler = function (e) {
+                    if (e && !e.entry) {
+                        e.entry = entry;
+                    }
+                    // Since $.when returns immediately if one
+                    // Deferred is rejected, we use resolve instead.
+                    // This allows valid files and invalid items
+                    // to be returned together in one set:
+                    dfd.resolve([e]);
+                },
+                successHandler = function (entries) {
+                    that._handleFileTreeEntries(
+                        entries,
+                        path + entry.name + '/'
+                    ).done(function (files) {
+                        dfd.resolve(files);
+                    }).fail(errorHandler);
+                },
+                readEntries = function () {
+                    dirReader.readEntries(function (results) {
+                        if (!results.length) {
+                            successHandler(entries);
+                        } else {
+                            entries = entries.concat(results);
+                            readEntries();
+                        }
+                    }, errorHandler);
+                },
+                dirReader, entries = [];
+            path = path || '';
+            if (entry.isFile) {
+                if (entry._file) {
+                    // Workaround for Chrome bug #149735
+                    entry._file.relativePath = path;
+                    dfd.resolve(entry._file);
+                } else {
+                    entry.file(function (file) {
+                        file.relativePath = path;
+                        dfd.resolve(file);
+                    }, errorHandler);
+                }
+            } else if (entry.isDirectory) {
+                dirReader = entry.createReader();
+                readEntries();
+            } else {
+                // Return an empy list for file system items
+                // other than files or directories:
+                dfd.resolve([]);
+            }
+            return dfd.promise();
+        },
+
+        _handleFileTreeEntries: function (entries, path) {
+            var that = this;
+            return $.when.apply(
+                $,
+                $.map(entries, function (entry) {
+                    return that._handleFileTreeEntry(entry, path);
+                })
+            ).then(function () {
+                return Array.prototype.concat.apply(
+                    [],
+                    arguments
+                );
+            });
+        },
+
+        _getDroppedFiles: function (dataTransfer) {
+            dataTransfer = dataTransfer || {};
+            var items = dataTransfer.items;
+            if (items && items.length && (items[0].webkitGetAsEntry ||
+                    items[0].getAsEntry)) {
+                return this._handleFileTreeEntries(
+                    $.map(items, function (item) {
+                        var entry;
+                        if (item.webkitGetAsEntry) {
+                            entry = item.webkitGetAsEntry();
+                            if (entry) {
+                                // Workaround for Chrome bug #149735:
+                                entry._file = item.getAsFile();
+                            }
+                            return entry;
+                        }
+                        return item.getAsEntry();
+                    })
+                );
+            }
+            return $.Deferred().resolve(
+                $.makeArray(dataTransfer.files)
+            ).promise();
+        },
+
+        _getSingleFileInputFiles: function (fileInput) {
+            fileInput = $(fileInput);
+            var entries = fileInput.prop('webkitEntries') ||
+                    fileInput.prop('entries'),
+                files,
+                value;
+            if (entries && entries.length) {
+                return this._handleFileTreeEntries(entries);
+            }
+            files = $.makeArray(fileInput.prop('files'));
+            if (!files.length) {
+                value = fileInput.prop('value');
+                if (!value) {
+                    return $.Deferred().resolve([]).promise();
+                }
+                // If the files property is not available, the browser does not
+                // support the File API and we add a pseudo File object with
+                // the input value as name with path information removed:
+                files = [{name: value.replace(/^.*\\/, '')}];
+            } else if (files[0].name === undefined && files[0].fileName) {
+                // File normalization for Safari 4 and Firefox 3:
+                $.each(files, function (index, file) {
+                    file.name = file.fileName;
+                    file.size = file.fileSize;
+                });
+            }
+            return $.Deferred().resolve(files).promise();
+        },
+
+        _getFileInputFiles: function (fileInput) {
+            if (!(fileInput instanceof $) || fileInput.length === 1) {
+                return this._getSingleFileInputFiles(fileInput);
+            }
+            return $.when.apply(
+                $,
+                $.map(fileInput, this._getSingleFileInputFiles)
+            ).then(function () {
+                return Array.prototype.concat.apply(
+                    [],
+                    arguments
+                );
+            });
+        },
+
+        _onChange: function (e) {
+            var that = this,
+                data = {
+                    fileInput: $(e.target),
+                    form: $(e.target.form)
+                };
+            this._getFileInputFiles(data.fileInput).always(function (files) {
+                data.files = files;
+                if (that.options.replaceFileInput) {
+                    that._replaceFileInput(data);
+                }
+                if (that._trigger(
+                        'change',
+                        $.Event('change', {delegatedEvent: e}),
+                        data
+                    ) !== false) {
+                    that._onAdd(e, data);
+                }
+            });
+        },
+
+        _onPaste: function (e) {
+            var items = e.originalEvent && e.originalEvent.clipboardData &&
+                    e.originalEvent.clipboardData.items,
+                data = {files: []};
+            if (items && items.length) {
+                $.each(items, function (index, item) {
+                    var file = item.getAsFile && item.getAsFile();
+                    if (file) {
+                        data.files.push(file);
+                    }
+                });
+                if (this._trigger(
+                        'paste',
+                        $.Event('paste', {delegatedEvent: e}),
+                        data
+                    ) !== false) {
+                    this._onAdd(e, data);
+                }
+            }
+        },
+
+        _onDrop: function (e) {
+            e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
+            var that = this,
+                dataTransfer = e.dataTransfer,
+                data = {};
+            if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
+                e.preventDefault();
+                this._getDroppedFiles(dataTransfer).always(function (files) {
+                    data.files = files;
+                    if (that._trigger(
+                            'drop',
+                            $.Event('drop', {delegatedEvent: e}),
+                            data
+                        ) !== false) {
+                        that._onAdd(e, data);
+                    }
+                });
+            } else {
+                // "dropnofiles" is triggered to allow proper cleanup of the
+                // drag and drop operation, as some browsers trigger "drop"
+                // events that have no files even if the "DataTransfer.types" of
+                // the "dragover" event included a "Files" item.
+                this._trigger(
+                    'dropnofiles',
+                    $.Event('drop', {delegatedEvent: e})
+                );
+            }
+        },
+
+        _onDragOver: getDragHandler('dragover'),
+
+        _onDragEnter: getDragHandler('dragenter'),
+
+        _onDragLeave: getDragHandler('dragleave'),
+
+        _initEventHandlers: function () {
+            if (this._isXHRUpload(this.options)) {
+                this._on(this.options.dropZone, {
+                    dragover: this._onDragOver,
+                    drop: this._onDrop,
+                    // event.preventDefault() on dragenter is required for IE10+:
+                    dragenter: this._onDragEnter,
+                    // dragleave is not required, but added for completeness:
+                    dragleave: this._onDragLeave
+                });
+                this._on(this.options.pasteZone, {
+                    paste: this._onPaste
+                });
+            }
+            if ($.support.fileInput) {
+                this._on(this.options.fileInput, {
+                    change: this._onChange
+                });
+            }
+        },
+
+        _destroyEventHandlers: function () {
+            this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
+            this._off(this.options.pasteZone, 'paste');
+            this._off(this.options.fileInput, 'change');
+        },
+
+        _setOption: function (key, value) {
+            var reinit = $.inArray(key, this._specialOptions) !== -1;
+            if (reinit) {
+                this._destroyEventHandlers();
+            }
+            this._super(key, value);
+            if (reinit) {
+                this._initSpecialOptions();
+                this._initEventHandlers();
+            }
+        },
+
+        _initSpecialOptions: function () {
+            var options = this.options;
+            if (options.fileInput === undefined) {
+                options.fileInput = this.element.is('input[type="file"]') ?
+                        this.element : this.element.find('input[type="file"]');
+            } else if (!(options.fileInput instanceof $)) {
+                options.fileInput = $(options.fileInput);
+            }
+            if (!(options.dropZone instanceof $)) {
+                options.dropZone = $(options.dropZone);
+            }
+            if (!(options.pasteZone instanceof $)) {
+                options.pasteZone = $(options.pasteZone);
+            }
+        },
+
+        _getRegExp: function (str) {
+            var parts = str.split('/'),
+                modifiers = parts.pop();
+            parts.shift();
+            return new RegExp(parts.join('/'), modifiers);
+        },
+
+        _isRegExpOption: function (key, value) {
+            return key !== 'url' && $.type(value) === 'string' &&
+                /^\/.*\/[igm]{0,3}$/.test(value);
+        },
+
+        _initDataAttributes: function () {
+            var that = this,
+                options = this.options,
+                data = this.element.data();
+            // Initialize options set via HTML5 data-attributes:
+            $.each(
+                this.element[0].attributes,
+                function (index, attr) {
+                    var key = attr.name.toLowerCase(),
+                        value;
+                    if (/^data-/.test(key)) {
+                        // Convert hyphen-ated key to camelCase:
+                        key = key.slice(5).replace(/-[a-z]/g, function (str) {
+                            return str.charAt(1).toUpperCase();
+                        });
+                        value = data[key];
+                        if (that._isRegExpOption(key, value)) {
+                            value = that._getRegExp(value);
+                        }
+                        options[key] = value;
+                    }
+                }
+            );
+        },
+
+        _create: function () {
+            this._initDataAttributes();
+            this._initSpecialOptions();
+            this._slots = [];
+            this._sequence = this._getXHRPromise(true);
+            this._sending = this._active = 0;
+            this._initProgressObject(this);
+            this._initEventHandlers();
+        },
+
+        // This method is exposed to the widget API and allows to query
+        // the number of active uploads:
+        active: function () {
+            return this._active;
+        },
+
+        // This method is exposed to the widget API and allows to query
+        // the widget upload progress.
+        // It returns an object with loaded, total and bitrate properties
+        // for the running uploads:
+        progress: function () {
+            return this._progress;
+        },
+
+        // This method is exposed to the widget API and allows adding files
+        // using the fileupload API. The data parameter accepts an object which
+        // must have a files property and can contain additional options:
+        // .fileupload('add', {files: filesList});
+        add: function (data) {
+            var that = this;
+            if (!data || this.options.disabled) {
+                return;
+            }
+            if (data.fileInput && !data.files) {
+                this._getFileInputFiles(data.fileInput).always(function (files) {
+                    data.files = files;
+                    that._onAdd(null, data);
+                });
+            } else {
+                data.files = $.makeArray(data.files);
+                this._onAdd(null, data);
+            }
+        },
+
+        // This method is exposed to the widget API and allows sending files
+        // using the fileupload API. The data parameter accepts an object which
+        // must have a files or fileInput property and can contain additional options:
+        // .fileupload('send', {files: filesList});
+        // The method returns a Promise object for the file upload call.
+        send: function (data) {
+            if (data && !this.options.disabled) {
+                if (data.fileInput && !data.files) {
+                    var that = this,
+                        dfd = $.Deferred(),
+                        promise = dfd.promise(),
+                        jqXHR,
+                        aborted;
+                    promise.abort = function () {
+                        aborted = true;
+                        if (jqXHR) {
+                            return jqXHR.abort();
+                        }
+                        dfd.reject(null, 'abort', 'abort');
+                        return promise;
+                    };
+                    this._getFileInputFiles(data.fileInput).always(
+                        function (files) {
+                            if (aborted) {
+                                return;
+                            }
+                            if (!files.length) {
+                                dfd.reject();
+                                return;
+                            }
+                            data.files = files;
+                            jqXHR = that._onSend(null, data);
+                            jqXHR.then(
+                                function (result, textStatus, jqXHR) {
+                                    dfd.resolve(result, textStatus, jqXHR);
+                                },
+                                function (jqXHR, textStatus, errorThrown) {
+                                    dfd.reject(jqXHR, textStatus, errorThrown);
+                                }
+                            );
+                        }
+                    );
+                    return this._enhancePromise(promise);
+                }
+                data.files = $.makeArray(data.files);
+                if (data.files.length) {
+                    return this._onSend(null, data);
+                }
+            }
+            return this._getXHRPromise(false, data && data.context);
+        }
+
+    });
+
+}));
+
+/*!
+ * jquery-visibility v1.0.11
+ * Page visibility shim for jQuery.
+ *
+ * Project Website: http://mths.be/visibility
+ *
+ * @version 1.0.11
+ * @license MIT.
+ * @author Mathias Bynens - @mathias
+ * @author Jan Paepke - @janpaepke
+ */
+;(function (root, factory) {
+	if (typeof define === 'function' && define.amd) {
+		// AMD. Register as an anonymous module.
+		define(['jquery'], function ($) {
+			return factory(root, $);
+		});
+	} else if (typeof exports === 'object') {
+		// Node/CommonJS
+		module.exports = factory(root, require('jquery'));
+	} else {
+		// Browser globals
+		factory(root, jQuery);
+	}
+}(this, function(window, $, undefined) {
+	"use strict";
+
+	var
+		document = window.document,
+		property, // property name of document, that stores page visibility
+		vendorPrefixes = ['webkit', 'o', 'ms', 'moz', ''],
+		$support = $.support || {},
+	// In Opera, `'onfocusin' in document == true`, hence the extra `hasFocus` check to detect IE-like behavior
+		eventName = 'onfocusin' in document && 'hasFocus' in document ?
+			'focusin focusout' :
+			'focus blur';
+
+	var prefix;
+	while ((prefix = vendorPrefixes.pop()) !== undefined) {
+		property = (prefix ? prefix + 'H': 'h') + 'idden';
+		$support.pageVisibility = document[property] !== undefined;
+		if ($support.pageVisibility) {
+			eventName = prefix + 'visibilitychange';
+			break;
+		}
+	}
+
+	// normalize to and update document hidden property
+	function updateState() {
+		if (property !== 'hidden') {
+			document.hidden = $support.pageVisibility ? document[property] : undefined;
+		}
+	}
+	updateState();
+
+	$(/blur$/.test(eventName) ? window : document).on(eventName, function(event) {
+		var type = event.type;
+		var originalEvent = event.originalEvent;
+
+		// Avoid errors from triggered native events for which `originalEvent` is
+		// not available.
+		if (!originalEvent) {
+			return;
+		}
+
+		var toElement = originalEvent.toElement;
+
+		// If it’s a `{focusin,focusout}` event (IE), `fromElement` and `toElement`
+		// should both be `null` or `undefined`; else, the page visibility hasn’t
+		// changed, but the user just clicked somewhere in the doc. In IE9, we need
+		// to check the `relatedTarget` property instead.
+		if (
+			!/^focus./.test(type) || (
+				toElement === undefined &&
+				originalEvent.fromElement === undefined &&
+				originalEvent.relatedTarget === undefined
+			)
+		) {
+			$(document).triggerHandler(
+				property && document[property] || /^(?:blur|focusout)$/.test(type) ?
+					'hide' :
+					'show'
+			);
+		}
+		// and update the current state
+		updateState();
+	});
+}));
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC, OCA) {
+
+	/**
+	 * @class OC.Files.FileInfo
+	 * @classdesc File information
+	 *
+	 * @param {Object} attributes file data
+	 * @param {int} attributes.id file id
+	 * @param {string} attributes.name file name
+	 * @param {string} attributes.path path leading to the file,
+	 * without the file name and with a leading slash
+	 * @param {int} attributes.size size
+	 * @param {string} attributes.mimetype mime type
+	 * @param {string} attributes.icon icon URL
+	 * @param {int} attributes.permissions permissions
+	 * @param {Date} attributes.mtime modification time
+	 * @param {string} attributes.etag etag
+	 * @param {string} mountType mount type
+	 *
+	 * @since 8.2
+	 */
+	var FileInfoModel = OC.Backbone.Model.extend({
+
+		defaults: {
+			mimetype: 'application/octet-stream',
+			path: ''
+		},
+
+		_filesClient: null,
+
+		initialize: function(data, options) {
+			if (!_.isUndefined(data.id)) {
+				data.id = parseInt(data.id, 10);
+			}
+
+			if( options ){
+				if (options.filesClient) {
+					this._filesClient = options.filesClient;
+				}
+			}
+		},
+
+		/**
+		 * Returns whether this file is a directory
+		 *
+		 * @return {boolean} true if this is a directory, false otherwise
+		 */
+		isDirectory: function() {
+			return this.get('mimetype') === 'httpd/unix-directory';
+		},
+
+		/**
+		 * Returns whether this file is an image
+		 *
+		 * @return {boolean} true if this is an image, false otherwise
+		 */
+		isImage: function() {
+			if (!this.has('mimetype')) {
+				return false;
+			}
+			return this.get('mimetype').substr(0, 6) === 'image/'
+				|| this.get('mimetype') === 'application/postscript'
+				|| this.get('mimetype') === 'application/illustrator'
+				|| this.get('mimetype') === 'application/x-photoshop';
+		},
+
+		/**
+		 * Returns the full path to this file
+		 *
+		 * @return {string} full path
+		 */
+		getFullPath: function() {
+			return OC.joinPaths(this.get('path'), this.get('name'));
+		},
+
+		/**
+		 * Reloads missing properties from server and set them in the model.
+		 * @param properties array of properties to be reloaded
+		 * @return ajax call object
+		 */
+		reloadProperties: function(properties) {
+			if( !this._filesClient ){
+				return;
+			}
+
+			var self = this;
+			var deferred = $.Deferred();
+
+			var targetPath = OC.joinPaths(this.get('path') + '/', this.get('name'));
+
+			this._filesClient.getFileInfo(targetPath, {
+					properties: properties
+				})
+				.then(function(status, data) {
+					// the following lines should be extracted to a mapper
+
+					if( properties.indexOf(OC.Files.Client.PROPERTY_GETCONTENTLENGTH) !== -1
+					||  properties.indexOf(OC.Files.Client.PROPERTY_SIZE) !== -1 ) {
+						self.set('size', data.size);
+					}
+
+					deferred.resolve(status, data);
+				})
+				.fail(function(status) {
+					OC.Notification.show(t('files', 'Could not load info for file "{file}"', {file: self.get('name')}), {type: 'error'});
+					deferred.reject(status);
+				});
+
+			return deferred.promise();
+		}
+	});
+
+	if (!OCA.Files) {
+		OCA.Files = {};
+	}
+	OCA.Files.FileInfoModel = FileInfoModel;
+
+})(OC, OCA);
+
+
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+(function() {
+	/**
+	 * The FileSummary class encapsulates the file summary values and
+	 * the logic to render it in the given container
+	 *
+	 * @constructs FileSummary
+	 * @memberof OCA.Files
+	 *
+	 * @param $tr table row element
+	 * @param {OC.Backbone.Model} [options.filesConfig] files app configuration
+	 */
+	var FileSummary = function($tr, options) {
+		options = options || {};
+		var self = this;
+		this.$el = $tr;
+		var filesConfig = options.config;
+		if (filesConfig) {
+			this._showHidden = !!filesConfig.get('showhidden');
+			filesConfig.on('change:showhidden', function() {
+				self._showHidden = !!this.get('showhidden');
+				self.update();
+			});
+		}
+		this.clear();
+		this.render();
+	};
+
+	FileSummary.prototype = {
+		_showHidden: null,
+
+		summary: {
+			totalFiles: 0,
+			totalDirs: 0,
+			totalHidden: 0,
+			totalSize: 0,
+			filter:'',
+			sumIsPending:false
+		},
+
+		/**
+		 * Returns whether the given file info must be hidden
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo file info
+		 * 
+		 * @return {boolean} true if the file is a hidden file, false otherwise
+		 */
+		_isHiddenFile: function(file) {
+			return file.name && file.name.charAt(0) === '.';
+		},
+
+		/**
+		 * Adds file
+		 * @param {OC.Files.FileInfo} file file to add
+		 * @param {boolean} update whether to update the display
+		 */
+		add: function(file, update) {
+			if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
+				return;
+			}
+			if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+				this.summary.totalDirs++;
+			}
+			else {
+				this.summary.totalFiles++;
+			}
+			if (this._isHiddenFile(file)) {
+				this.summary.totalHidden++;
+			}
+
+			var size = parseInt(file.size, 10) || 0;
+			if (size >=0) {
+				this.summary.totalSize += size;
+			} else {
+				this.summary.sumIsPending = true;
+			}
+			if (!!update) {
+				this.update();
+			}
+		},
+		/**
+		 * Removes file
+		 * @param {OC.Files.FileInfo} file file to remove
+		 * @param {boolean} update whether to update the display
+		 */
+		remove: function(file, update) {
+			if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
+				return;
+			}
+			if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+				this.summary.totalDirs--;
+			}
+			else {
+				this.summary.totalFiles--;
+			}
+			if (this._isHiddenFile(file)) {
+				this.summary.totalHidden--;
+			}
+			var size = parseInt(file.size, 10) || 0;
+			if (size >=0) {
+				this.summary.totalSize -= size;
+			}
+			if (!!update) {
+				this.update();
+			}
+		},
+		setFilter: function(filter, files){
+			this.summary.filter = filter.toLowerCase();
+			this.calculate(files);
+		},
+		/**
+		 * Returns the total of files and directories
+		 */
+		getTotal: function() {
+			return this.summary.totalDirs + this.summary.totalFiles;
+		},
+		/**
+		 * Recalculates the summary based on the given files array
+		 * @param files array of files
+		 */
+		calculate: function(files) {
+			var file;
+			var summary = {
+				totalDirs: 0,
+				totalFiles: 0,
+				totalHidden: 0,
+				totalSize: 0,
+				filter: this.summary.filter,
+				sumIsPending: false
+			};
+
+			for (var i = 0; i < files.length; i++) {
+				file = files[i];
+				if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) {
+					continue;
+				}
+				if (file.type === 'dir' || file.mime === 'httpd/unix-directory') {
+					summary.totalDirs++;
+				}
+				else {
+					summary.totalFiles++;
+				}
+				if (this._isHiddenFile(file)) {
+					summary.totalHidden++;
+				}
+				var size = parseInt(file.size, 10) || 0;
+				if (size >=0) {
+					summary.totalSize += size;
+				} else {
+					summary.sumIsPending = true;
+				}
+			}
+			this.setSummary(summary);
+		},
+		/**
+		 * Clears the summary
+		 */
+		clear: function() {
+			this.calculate([]);
+		},
+		/**
+		 * Sets the current summary values
+		 * @param summary map
+		 */
+		setSummary: function(summary) {
+			this.summary = summary;
+			if (typeof this.summary.filter === 'undefined') {
+				this.summary.filter = '';
+			}
+			this.update();
+		},
+
+		_infoTemplate: function(data) {
+			/* NOTE: To update the template make changes in filesummary.handlebars
+			 * and run:
+			 *
+			 * handlebars -n OCA.Files.FileSummary.Templates filesummary.handlebars -f filesummary_template.js
+			 */
+			return OCA.Files.Templates['filesummary'](_.extend({
+				connectorLabel: t('files', '{dirs} and {files}', {dirs: '', files: ''})
+			}, data));
+		},
+
+		/**
+		 * Renders the file summary element
+		 */
+		update: function() {
+			if (!this.$el) {
+				return;
+			}
+			if (!this.summary.totalFiles && !this.summary.totalDirs) {
+				this.$el.addClass('hidden');
+				return;
+			}
+			// There's a summary and data -> Update the summary
+			this.$el.removeClass('hidden');
+			var $dirInfo = this.$el.find('.dirinfo');
+			var $fileInfo = this.$el.find('.fileinfo');
+			var $connector = this.$el.find('.connector');
+			var $filterInfo = this.$el.find('.filter');
+			var $hiddenInfo = this.$el.find('.hiddeninfo');
+
+			// Substitute old content with new translations
+			$dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs));
+			$fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles));
+			$hiddenInfo.html(' (' + n('files', 'including %n hidden', 'including %n hidden', this.summary.totalHidden) + ')');
+			var fileSize = this.summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(this.summary.totalSize);
+			this.$el.find('.filesize').html(fileSize);
+
+			// Show only what's necessary (may be hidden)
+			if (this.summary.totalDirs === 0) {
+				$dirInfo.addClass('hidden');
+				$connector.addClass('hidden');
+			} else {
+				$dirInfo.removeClass('hidden');
+			}
+			if (this.summary.totalFiles === 0) {
+				$fileInfo.addClass('hidden');
+				$connector.addClass('hidden');
+			} else {
+				$fileInfo.removeClass('hidden');
+			}
+			if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) {
+				$connector.removeClass('hidden');
+			}
+			$hiddenInfo.toggleClass('hidden', this.summary.totalHidden === 0 || this._showHidden)
+			if (this.summary.filter === '') {
+				$filterInfo.html('');
+				$filterInfo.addClass('hidden');
+			} else {
+				$filterInfo.html(' ' + n('files', 'matches \'{filter}\'', 'match \'{filter}\'', this.summary.totalDirs + this.summary.totalFiles, {filter: this.summary.filter}));
+				$filterInfo.removeClass('hidden');
+			}
+		},
+		render: function() {
+			if (!this.$el) {
+				return;
+			}
+			var summary = this.summary;
+
+			// don't show the filesize column, if filesize is NaN (e.g. in trashbin)
+			var fileSize = '';
+			if (!isNaN(summary.totalSize)) {
+				fileSize = summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(summary.totalSize);
+				fileSize = '<td class="filesize">' + fileSize + '</td>';
+			}
+
+			var $summary = $(
+				'<td>' + this._infoTemplate() + '</td>' +
+				fileSize +
+				'<td class="date"></td>'
+			);
+			this.$el.addClass('hidden');
+			this.$el.append($summary);
+			this.update();
+		}
+	};
+	OCA.Files.FileSummary = FileSummary;
+})();
+
+
+
+/*
+ * Copyright (c) 2018
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	var FileMultiSelectMenu = OC.Backbone.View.extend({
+		tagName: 'div',
+		className: 'filesSelectMenu popovermenu bubble menu-center',
+		_scopes: null,
+		initialize: function(menuItems) {
+			this._scopes = menuItems;
+		},
+		events: {
+			'click a.action': '_onClickAction'
+		},
+
+		/**
+		 * Renders the menu with the currently set items
+		 */
+		render: function() {
+			this.$el.html(OCA.Files.Templates['filemultiselectmenu']({
+				items: this._scopes
+			}));
+		},
+		/**
+		 * Displays the menu under the given element
+		 *
+		 * @param {OCA.Files.FileActionContext} context context
+		 * @param {Object} $trigger trigger element
+		 */
+		show: function(context) {
+			this._context = context;
+			this.render();
+			this.$el.removeClass('hidden');
+			if (window.innerWidth < 480) {
+				this.$el.removeClass('menu-center').addClass('menu-right');
+			} else {
+				this.$el.removeClass('menu-right').addClass('menu-center');
+			}
+			OC.showMenu(null, this.$el);
+			return false;
+		},
+		toggleItemVisibility: function (itemName, show) {
+			if (show) {
+				this.$el.find('.item-' + itemName).removeClass('hidden');
+			} else {
+				this.$el.find('.item-' + itemName).addClass('hidden');
+			}
+		},
+		updateItemText: function (itemName, translation) {
+			this.$el.find('.item-' + itemName).find('.label').text(translation);
+		},
+		toggleLoading: function (itemName, showLoading) {
+			var $actionElement = this.$el.find('.item-' + itemName);
+			if ($actionElement.length === 0) {
+				return;
+			}
+			var $icon = $actionElement.find('.icon');
+			if (showLoading) {
+				var $loadingIcon = $('<span class="icon icon-loading-small"></span>');
+				$icon.after($loadingIcon);
+				$icon.addClass('hidden');
+				$actionElement.addClass('disabled');
+			} else {
+				$actionElement.find('.icon-loading-small').remove();
+				$actionElement.find('.icon').removeClass('hidden');
+				$actionElement.removeClass('disabled');
+			}
+		},
+		isDisabled: function (itemName) {
+			var $actionElement = this.$el.find('.item-' + itemName);
+			return $actionElement.hasClass('disabled');
+		},
+		/**
+		 * Event handler whenever an action has been clicked within the menu
+		 *
+		 * @param {Object} event event object
+		 */
+		_onClickAction: function (event) {
+			var $target = $(event.currentTarget);
+			if (!$target.hasClass('menuitem')) {
+				$target = $target.closest('.menuitem');
+			}
+
+			OC.hideMenus();
+			this._context.multiSelectMenuClick(event, $target.data('action'));
+			return false;
+		}
+	});
+
+	OCA.Files.FileMultiSelectMenu = FileMultiSelectMenu;
+})(OC, OCA);
+
+
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+(function() {
+	/**
+	 * @class BreadCrumb
+	 * @memberof OCA.Files
+	 * @classdesc Breadcrumbs that represent the current path.
+	 *
+	 * @param {Object} [options] options
+	 * @param {Function} [options.onClick] click event handler
+	 * @param {Function} [options.onDrop] drop event handler
+	 * @param {Function} [options.getCrumbUrl] callback that returns
+	 * the URL of a given breadcrumb
+	 */
+	var BreadCrumb = function(options){
+		this.$el = $('<div class="breadcrumb"></div>');
+		this.$menu = $('<div class="popovermenu menu-center"><ul></ul></div>');
+
+		this.crumbSelector = '.crumb:not(.hidden):not(.crumbhome):not(.crumbmenu)';
+		this.hiddenCrumbSelector = '.crumb.hidden:not(.crumbhome):not(.crumbmenu)';
+		options = options || {};
+		if (options.onClick) {
+			this.onClick = options.onClick;
+		}
+		if (options.onDrop) {
+			this.onDrop = options.onDrop;
+			this.onOver = options.onOver;
+			this.onOut = options.onOut;
+		}
+		if (options.getCrumbUrl) {
+			this.getCrumbUrl = options.getCrumbUrl;
+		}
+		this._detailViews = [];
+	};
+
+	/**
+	 * @memberof OCA.Files
+	 */
+	BreadCrumb.prototype = {
+		$el: null,
+		dir: null,
+		dirInfo: null,
+
+		/**
+		 * Total width of all breadcrumbs
+		 * @type int
+		 * @private
+		 */
+		totalWidth: 0,
+		breadcrumbs: [],
+		onClick: null,
+		onDrop: null,
+		onOver: null,
+		onOut: null,
+
+		/**
+		 * Sets the directory to be displayed as breadcrumb.
+		 * This will re-render the breadcrumb.
+		 * @param dir path to be displayed as breadcrumb
+		 */
+		setDirectory: function(dir) {
+			dir = dir.replace(/\\/g, '/');
+			dir = dir || '/';
+			if (dir !== this.dir) {
+				this.dir = dir;
+				this.render();
+			}
+		},
+
+		setDirectoryInfo: function(dirInfo) {
+			if (dirInfo !== this.dirInfo) {
+				this.dirInfo = dirInfo;
+				this.render();
+			}
+		},
+
+		/**
+		 * @param {Backbone.View} detailView
+		 */
+		addDetailView: function(detailView) {
+			this._detailViews.push(detailView);
+		},
+
+		/**
+		 * Returns the full URL to the given directory
+		 *
+		 * @param {Object.<String, String>} part crumb data as map
+		 * @param {int} index crumb index
+		 * @return full URL
+		 */
+		getCrumbUrl: function(part, index) {
+			return '#';
+		},
+
+		/**
+		 * Renders the breadcrumb elements
+		 */
+		render: function() {
+			// Menu is destroyed on every change, we need to init it
+			OC.unregisterMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
+
+			var parts = this._makeCrumbs(this.dir || '/');
+			var $crumb;
+			var $menuItem;
+			this.$el.empty();
+			this.breadcrumbs = [];
+
+			for (var i = 0; i < parts.length; i++) {
+				var part = parts[i];
+				var $image;
+				var $link = $('<a></a>');
+				$crumb = $('<div class="crumb svg"></div>');
+				if(part.dir) {
+					$link.attr('href', this.getCrumbUrl(part, i));
+				}
+				if(part.name) {
+					$link.text(part.name);
+				}
+				$link.addClass(part.linkclass);
+				$crumb.append($link);
+				$crumb.data('dir', part.dir);
+				// Ignore menu button
+				$crumb.data('crumb-id', i - 1);
+				$crumb.addClass(part.class);
+
+				if (part.img) {
+					$image = $('<img class="svg"></img>');
+					$image.attr('src', part.img);
+					$image.attr('alt', part.alt);
+					$link.append($image);
+				}
+				this.breadcrumbs.push($crumb);
+				this.$el.append($crumb);
+				// Only add feedback if not menu
+				if (this.onClick && i !== 0) {
+					$link.on('click', this.onClick);
+				}
+			}
+
+			// Menu creation
+			this._createMenu();
+			for (var j = 0; j < parts.length; j++) {
+				var menuPart = parts[j];
+				if(menuPart.dir) {
+					$menuItem = $('<li class="crumblist"><a><span class="icon-folder"></span><span></span></a></li>');
+					$menuItem.data('dir', menuPart.dir);
+					$menuItem.find('a').attr('href', this.getCrumbUrl(part, j));
+					$menuItem.find('span:eq(1)').text(menuPart.name);
+					this.$menu.children('ul').append($menuItem);
+					if (this.onClick) {
+						$menuItem.on('click', this.onClick);
+					}
+				}
+			}
+			_.each(this._detailViews, function(view) {
+				view.render({
+					dirInfo: this.dirInfo
+				});
+				$crumb.append(view.$el);
+			}, this);
+
+			// setup drag and drop
+			if (this.onDrop) {
+				this.$el.find('.crumb:not(:last-child):not(.crumbmenu), .crumblist:not(:last-child)').droppable({
+					drop: this.onDrop,
+					over: this.onOver,
+					out: this.onOut,
+					tolerance: 'pointer',
+					hoverClass: 'canDrop',
+					greedy: true
+				});
+			}
+
+			// Menu is destroyed on every change, we need to init it
+			OC.registerMenu($('.crumbmenu > .icon-more'), $('.crumbmenu > .popovermenu'));
+
+			this._resize();
+		},
+
+		/**
+		 * Makes a breadcrumb structure based on the given path
+		 *
+		 * @param {String} dir path to split into a breadcrumb structure
+		 * @return {Object.<String, String>} map of {dir: path, name: displayName}
+		 */
+		_makeCrumbs: function(dir) {
+			var crumbs = [];
+			var pathToHere = '';
+			// trim leading and trailing slashes
+			dir = dir.replace(/^\/+|\/+$/g, '');
+			var parts = dir.split('/');
+			if (dir === '') {
+				parts = [];
+			}
+			// menu part
+			crumbs.push({
+				class: 'crumbmenu hidden',
+				linkclass: 'icon-more menutoggle'
+			});
+			// root part
+			crumbs.push({
+				name: t('core', 'Home'),
+				dir: '/',
+				class: 'crumbhome',
+				linkclass: 'icon-home'
+			});
+			for (var i = 0; i < parts.length; i++) {
+				var part = parts[i];
+				pathToHere = pathToHere + '/' + part;
+				crumbs.push({
+					dir: pathToHere,
+					name: part
+				});
+			}
+			return crumbs;
+		},
+
+		/**
+		 * Calculate real width based on individual crumbs
+		 *
+		 * @param {boolean} ignoreHidden ignore hidden crumbs
+		 */
+		getTotalWidth: function(ignoreHidden) {
+			// The width has to be calculated by adding up the width of all the
+			// crumbs; getting the width of the breadcrumb element is not a
+			// valid approach, as the returned value could be clamped to its
+			// parent width.
+			var totalWidth = 0;
+			for (var i = 0; i < this.breadcrumbs.length; i++ ) {
+				var $crumb = $(this.breadcrumbs[i]);
+				if(!$crumb.hasClass('hidden') || ignoreHidden === true) {
+					totalWidth += $crumb.outerWidth(true);
+				}
+			}
+			return totalWidth;
+		},
+
+ 		/**
+ 		 * Hide the middle crumb
+ 		 */
+ 		_hideCrumb: function() {
+			var length = this.$el.find(this.crumbSelector).length;
+			// Get the middle one floored down
+			var elmt = Math.floor(length / 2 - 0.5);
+			this.$el.find(this.crumbSelector+':eq('+elmt+')').addClass('hidden');
+ 		},
+
+ 		/**
+ 		 * Get the crumb to show
+ 		 */
+ 		_getCrumbElement: function() {
+			var hidden = this.$el.find(this.hiddenCrumbSelector).length;
+			var shown = this.$el.find(this.crumbSelector).length;
+			// Get the outer one with priority to the highest
+			var elmt = (1 - shown % 2) * (hidden - 1);
+			return this.$el.find(this.hiddenCrumbSelector + ':eq('+elmt+')');
+		},
+
+ 		/**
+ 		 * Show the middle crumb
+ 		 */
+ 		_showCrumb: function() {
+			if(this.$el.find(this.hiddenCrumbSelector).length === 1) {
+				this.$el.find(this.hiddenCrumbSelector).removeClass('hidden');
+			}
+			this._getCrumbElement().removeClass('hidden');
+ 		},
+
+		/**
+		 * Create and append the popovermenu
+		 */
+		_createMenu: function() {
+			this.$el.find('.crumbmenu').append(this.$menu);
+			this.$menu.children('ul').empty();
+		},
+
+		/**
+		 * Update the popovermenu
+		 */
+		_updateMenu: function() {
+			var menuItems = this.$el.find(this.hiddenCrumbSelector);
+
+			this.$menu.find('li').addClass('in-breadcrumb');
+			for (var i = 0; i < menuItems.length; i++) {
+				var crumbId = $(menuItems[i]).data('crumb-id');
+				this.$menu.find('li:eq('+crumbId+')').removeClass('in-breadcrumb');
+			}
+		},
+
+		_resize: function() {
+
+			if (this.breadcrumbs.length <= 2) {
+				// home & menu
+				return;
+			}
+
+			// Always hide the menu to ensure that it does not interfere with
+			// the width calculations; otherwise, the result could be different
+			// depending on whether the menu was previously being shown or not.
+			this.$el.find('.crumbmenu').addClass('hidden');
+
+			// Show the crumbs to compress the siblings before hidding again the
+			// crumbs. This is needed when the siblings expand to fill all the
+			// available width, as in that case their old width would limit the
+			// available width for the crumbs.
+			// Note that the crumbs shown always overflow the parent width
+			// (except, of course, when they all fit in).
+			while (this.$el.find(this.hiddenCrumbSelector).length > 0
+				&& this.getTotalWidth() <= this.$el.parent().width()) {
+				this._showCrumb();
+			}
+
+			var siblingsWidth = 0;
+			this.$el.prevAll(':visible').each(function () {
+				siblingsWidth += $(this).outerWidth(true);
+			});
+			this.$el.nextAll(':visible').each(function () {
+				siblingsWidth += $(this).outerWidth(true);
+			});
+
+			var availableWidth = this.$el.parent().width() - siblingsWidth;
+
+			// If container is smaller than content
+			// AND if there are crumbs left to hide
+			while (this.getTotalWidth() > availableWidth
+				&& this.$el.find(this.crumbSelector).length > 0) {
+				// As soon as one of the crumbs is hidden the menu will be
+				// shown. This is needed for proper results in further width
+				// checks.
+				// Note that the menu is not shown only when all the crumbs were
+				// being shown and they all fit the available space; if any of
+				// the crumbs was not being shown then those shown would
+				// overflow the available width, so at least one will be hidden
+				// and thus the menu will be shown.
+				this.$el.find('.crumbmenu').removeClass('hidden');
+				this._hideCrumb();
+			}
+
+			this._updateMenu();
+		}
+	};
+
+	OCA.Files.BreadCrumb = BreadCrumb;
+})();
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	/**
+	 * @class OCA.Files.FileList
+	 * @classdesc
+	 *
+	 * The FileList class manages a file list view.
+	 * A file list view consists of a controls bar and
+	 * a file list table.
+	 *
+	 * @param $el container element with existing markup for the #controls
+	 * and a table
+	 * @param {Object} [options] map of options, see other parameters
+	 * @param {Object} [options.scrollContainer] scrollable container, defaults to $(window)
+	 * @param {Object} [options.dragOptions] drag options, disabled by default
+	 * @param {Object} [options.folderDropOptions] folder drop options, disabled by default
+	 * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view
+	 * @param {boolean} [options.enableUpload=false] whether to enable uploader
+	 * @param {OC.Files.Client} [options.filesClient] files client to use
+	 */
+	var FileList = function($el, options) {
+		this.initialize($el, options);
+	};
+	/**
+	 * @memberof OCA.Files
+	 */
+	FileList.prototype = {
+		SORT_INDICATOR_ASC_CLASS: 'icon-triangle-n',
+		SORT_INDICATOR_DESC_CLASS: 'icon-triangle-s',
+
+		id: 'files',
+		appName: t('files', 'Files'),
+		isEmpty: true,
+		useUndo:true,
+
+		/**
+		 * Top-level container with controls and file list
+		 */
+		$el: null,
+
+		/**
+		 * Files table
+		 */
+		$table: null,
+
+		/**
+		 * List of rows (table tbody)
+		 */
+		$fileList: null,
+
+		/**
+		 * @type OCA.Files.BreadCrumb
+		 */
+		breadcrumb: null,
+
+		/**
+		 * @type OCA.Files.FileSummary
+		 */
+		fileSummary: null,
+
+		/**
+		 * @type OCA.Files.DetailsView
+		 */
+		_detailsView: null,
+
+		/**
+		 * Files client instance
+		 *
+		 * @type OC.Files.Client
+		 */
+		filesClient: null,
+
+		/**
+		 * Whether the file list was initialized already.
+		 * @type boolean
+		 */
+		initialized: false,
+
+		/**
+		 * Wheater the file list was already shown once
+		 * @type boolean
+		 */
+		shown: false,
+
+		/**
+		 * Number of files per page
+		 * Always show a minimum of 1
+		 *
+		 * @return {int} page size
+		 */
+		pageSize: function() {
+			var isGridView = this.$showGridView.is(':checked');
+			var columns = 1;
+			var rows = Math.ceil(this.$container.height() / 50);
+			if (isGridView) {
+				columns = Math.ceil(this.$container.width() / 160);
+				rows = Math.ceil(this.$container.height() / 160);
+			}
+			return Math.max(columns*rows, columns);
+		},
+
+		/**
+		 * Array of files in the current folder.
+		 * The entries are of file data.
+		 *
+		 * @type Array.<OC.Files.FileInfo>
+		 */
+		files: [],
+
+		/**
+		 * Current directory entry
+		 *
+		 * @type OC.Files.FileInfo
+		 */
+		dirInfo: null,
+
+		/**
+		 * File actions handler, defaults to OCA.Files.FileActions
+		 * @type OCA.Files.FileActions
+		 */
+		fileActions: null,
+		/**
+		 * File selection menu, defaults to OCA.Files.FileSelectionMenu
+		 * @type OCA.Files.FileSelectionMenu
+		 */
+		fileMultiSelectMenu: null,
+		/**
+		 * Whether selection is allowed, checkboxes and selection overlay will
+		 * be rendered
+		 */
+		_allowSelection: true,
+
+		/**
+		 * Map of file id to file data
+		 * @type Object.<int, Object>
+		 */
+		_selectedFiles: {},
+
+		/**
+		 * Summary of selected files.
+		 * @type OCA.Files.FileSummary
+		 */
+		_selectionSummary: null,
+
+		/**
+		 * If not empty, only files containing this string will be shown
+		 * @type String
+		 */
+		_filter: '',
+
+		/**
+		 * @type Backbone.Model
+		 */
+		_filesConfig: undefined,
+
+		/**
+		 * Sort attribute
+		 * @type String
+		 */
+		_sort: 'name',
+
+		/**
+		 * Sort direction: 'asc' or 'desc'
+		 * @type String
+		 */
+		_sortDirection: 'asc',
+
+		/**
+		 * Sort comparator function for the current sort
+		 * @type Function
+		 */
+		_sortComparator: null,
+
+		/**
+		 * Whether to do a client side sort.
+		 * When false, clicking on a table header will call reload().
+		 * When true, clicking on a table header will simply resort the list.
+		 */
+		_clientSideSort: true,
+
+		/**
+		 * Whether or not users can change the sort attribute or direction
+		 */
+		_allowSorting: true,
+
+		/**
+		 * Current directory
+		 * @type String
+		 */
+		_currentDirectory: null,
+
+		_dragOptions: null,
+		_folderDropOptions: null,
+
+		/**
+		 * @type OC.Uploader
+		 */
+		_uploader: null,
+
+		/**
+		 * Initialize the file list and its components
+		 *
+		 * @param $el container element with existing markup for the #controls
+		 * and a table
+		 * @param options map of options, see other parameters
+		 * @param options.scrollContainer scrollable container, defaults to $(window)
+		 * @param options.dragOptions drag options, disabled by default
+		 * @param options.folderDropOptions folder drop options, disabled by default
+		 * @param options.scrollTo name of file to scroll to after the first load
+		 * @param {OC.Files.Client} [options.filesClient] files API client
+		 * @param {OC.Backbone.Model} [options.filesConfig] files app configuration
+		 * @private
+		 */
+		initialize: function($el, options) {
+			var self = this;
+			options = options || {};
+			if (this.initialized) {
+				return;
+			}
+
+			if (options.shown) {
+				this.shown = options.shown;
+			}
+
+			if (options.config) {
+				this._filesConfig = options.config;
+			} else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) {
+				this._filesConfig = OCA.Files.App.getFilesConfig();
+			} else {
+				this._filesConfig = new OC.Backbone.Model({
+					'showhidden': false
+				});
+			}
+
+			if (options.dragOptions) {
+				this._dragOptions = options.dragOptions;
+			}
+			if (options.folderDropOptions) {
+				this._folderDropOptions = options.folderDropOptions;
+			}
+			if (options.filesClient) {
+				this.filesClient = options.filesClient;
+			} else {
+				// default client if not specified
+				this.filesClient = OC.Files.getClient();
+			}
+
+			this.$el = $el;
+			if (options.id) {
+				this.id = options.id;
+			}
+			this.$container = options.scrollContainer || $(window);
+			this.$table = $el.find('table:first');
+			this.$fileList = $el.find('#fileList');
+
+			if (!_.isUndefined(this._filesConfig)) {
+				this._filesConfig.on('change:showhidden', function() {
+					var showHidden = this.get('showhidden');
+					self.$el.toggleClass('hide-hidden-files', !showHidden);
+					self.updateSelectionSummary();
+
+					if (!showHidden) {
+						// hiding files could make the page too small, need to try rendering next page
+						self._onScroll();
+					}
+				});
+
+				this.$el.toggleClass('hide-hidden-files', !this._filesConfig.get('showhidden'));
+			}
+
+
+			if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
+				this._detailsView = new OCA.Files.DetailsView();
+				this._detailsView.$el.addClass('disappear');
+			}
+
+			this._initFileActions(options.fileActions);
+
+			if (this._detailsView) {
+				this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions}));
+			}
+
+			this.files = [];
+			this._selectedFiles = {};
+			this._selectionSummary = new OCA.Files.FileSummary(undefined, {config: this._filesConfig});
+			// dummy root dir info
+			this.dirInfo = new OC.Files.FileInfo({});
+
+			this.fileSummary = this._createSummary();
+
+			if (options.multiSelectMenu) {
+				this.multiSelectMenuItems = options.multiSelectMenu;
+				for (var i=0; i<this.multiSelectMenuItems.length; i++) {
+					if (_.isFunction(this.multiSelectMenuItems[i])) {
+						this.multiSelectMenuItems[i] = this.multiSelectMenuItems[i](this);
+					}
+				}
+				this.fileMultiSelectMenu = new OCA.Files.FileMultiSelectMenu(this.multiSelectMenuItems);
+				this.fileMultiSelectMenu.render();
+				this.$el.find('.selectedActions').append(this.fileMultiSelectMenu.$el);
+			}
+
+			if (options.sorting) {
+				this.setSort(options.sorting.mode, options.sorting.direction, false, false);
+			} else {
+				this.setSort('name', 'asc', false, false);
+			}
+
+			var breadcrumbOptions = {
+				onClick: _.bind(this._onClickBreadCrumb, this),
+				getCrumbUrl: function(part) {
+					return self.linkTo(part.dir);
+				}
+			};
+			// if dropping on folders is allowed, then also allow on breadcrumbs
+			if (this._folderDropOptions) {
+				breadcrumbOptions.onDrop = _.bind(this._onDropOnBreadCrumb, this);
+				breadcrumbOptions.onOver = function() {
+					self.$el.find('td.filename.ui-droppable').droppable('disable');
+				};
+				breadcrumbOptions.onOut = function() {
+					self.$el.find('td.filename.ui-droppable').droppable('enable');
+				};
+			}
+			this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
+
+			var $controls = this.$el.find('#controls');
+			if ($controls.length > 0) {
+				$controls.prepend(this.breadcrumb.$el);
+				this.$table.addClass('has-controls');
+			}
+
+			this._renderNewButton();
+
+			this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
+
+			// Toggle for grid view, only register once
+			this.$showGridView = $('input#showgridview:not(.registered)');
+			this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
+			this.$showGridView.addClass('registered');
+			$('#view-toggle').tooltip({placement: 'bottom', trigger: 'hover'});
+
+			this._onResize = _.debounce(_.bind(this._onResize, this), 250);
+			$('#app-content').on('appresized', this._onResize);
+			$(window).resize(this._onResize);
+
+			this.$el.on('show', this._onResize);
+
+			this.updateSearch();
+
+			this.$fileList.on('click','td.filename>a.name, td.filesize, td.date', _.bind(this._onClickFile, this));
+
+			this.$fileList.on("droppedOnFavorites", function (event, file) {
+				self.fileActions.triggerAction('Favorite', self.getModelForFile(file), self);
+			});
+
+			this.$fileList.on('droppedOnTrash', function (event, filename, directory) {
+				self.do_delete(filename, directory);
+			});
+
+			this.$fileList.on('change', 'td.selection>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
+			this.$fileList.on('mouseover', 'td.selection', _.bind(this._onMouseOverCheckbox, this));
+			this.$el.on('show', _.bind(this._onShow, this));
+			this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
+			this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this));
+			this.$el.find('.actions-selected').click(function () {
+				self.fileMultiSelectMenu.show(self);
+				return false;
+			});
+
+			this.$container.on('scroll', _.bind(this._onScroll, this));
+
+			if (options.scrollTo) {
+				this.$fileList.one('updated', function() {
+					self.scrollTo(options.scrollTo);
+				});
+			}
+
+			this._operationProgressBar = new OCA.Files.OperationProgressBar();
+			this._operationProgressBar.render();
+			this.$el.find('#uploadprogresswrapper').replaceWith(this._operationProgressBar.$el);
+
+			if (options.enableUpload) {
+				// TODO: auto-create this element
+				var $uploadEl = this.$el.find('#file_upload_start');
+				if ($uploadEl.exists()) {
+					this._uploader = new OC.Uploader($uploadEl, {
+						progressBar: this._operationProgressBar,
+						fileList: this,
+						filesClient: this.filesClient,
+						dropZone: $('#content'),
+						maxChunkSize: options.maxChunkSize
+					});
+
+					this.setupUploadEvents(this._uploader);
+				}
+			}
+
+
+			OC.Plugins.attach('OCA.Files.FileList', this);
+		},
+
+		/**
+		 * Destroy / uninitialize this instance.
+		 */
+		destroy: function() {
+			if (this._newFileMenu) {
+				this._newFileMenu.remove();
+			}
+			if (this._newButton) {
+				this._newButton.remove();
+			}
+			if (this._detailsView) {
+				this._detailsView.remove();
+			}
+			// TODO: also unregister other event handlers
+			this.fileActions.off('registerAction', this._onFileActionsUpdated);
+			this.fileActions.off('setDefault', this._onFileActionsUpdated);
+			OC.Plugins.detach('OCA.Files.FileList', this);
+			$('#app-content').off('appresized', this._onResize);
+		},
+
+		_selectionMode: 'single',
+		_getCurrentSelectionMode: function () {
+			return this._selectionMode;
+		},
+		_onClickToggleSelectionMode: function () {
+			this._selectionMode = (this._selectionMode === 'range') ? 'single' : 'range';
+			if (this._selectionMode === 'single') {
+				this._removeHalfSelection();
+			}
+		},
+
+		multiSelectMenuClick: function (ev, action) {
+				var actionFunction = _.find(this.multiSelectMenuItems, function (item) {return item.name === action;}).action;
+				if (actionFunction) {
+					actionFunction(ev);
+					return;
+				}
+				switch (action) {
+					case 'delete':
+						this._onClickDeleteSelected(ev)
+						break;
+					case 'download':
+						this._onClickDownloadSelected(ev);
+						break;
+					case 'copyMove':
+						this._onClickCopyMoveSelected(ev);
+						break;
+					case 'restore':
+						this._onClickRestoreSelected(ev);
+						break;
+				}
+		},
+		/**
+		 * Initializes the file actions, set up listeners.
+		 *
+		 * @param {OCA.Files.FileActions} fileActions file actions
+		 */
+		_initFileActions: function(fileActions) {
+			var self = this;
+			this.fileActions = fileActions;
+			if (!this.fileActions) {
+				this.fileActions = new OCA.Files.FileActions();
+				this.fileActions.registerDefaultActions();
+			}
+
+			if (this._detailsView) {
+				this.fileActions.registerAction({
+					name: 'Details',
+					displayName: t('files', 'Details'),
+					mime: 'all',
+					order: -50,
+					iconClass: 'icon-details',
+					permissions: OC.PERMISSION_NONE,
+					actionHandler: function(fileName, context) {
+						self._updateDetailsView(fileName);
+					}
+				});
+			}
+
+			this._onFileActionsUpdated = _.debounce(_.bind(this._onFileActionsUpdated, this), 100);
+			this.fileActions.on('registerAction', this._onFileActionsUpdated);
+			this.fileActions.on('setDefault', this._onFileActionsUpdated);
+		},
+
+		/**
+		 * Returns a unique model for the given file name.
+		 *
+		 * @param {string|object} fileName file name or jquery row
+		 * @return {OCA.Files.FileInfoModel} file info model
+		 */
+		getModelForFile: function(fileName) {
+			var self = this;
+			var $tr;
+			// jQuery object ?
+			if (fileName.is) {
+				$tr = fileName;
+				fileName = $tr.attr('data-file');
+			} else {
+				$tr = this.findFileEl(fileName);
+			}
+
+			if (!$tr || !$tr.length) {
+				return null;
+			}
+
+			// if requesting the selected model, return it
+			if (this._currentFileModel && this._currentFileModel.get('name') === fileName) {
+				return this._currentFileModel;
+			}
+
+			// TODO: note, this is a temporary model required for synchronising
+			// state between different views.
+			// In the future the FileList should work with Backbone.Collection
+			// and contain existing models that can be used.
+			// This method would in the future simply retrieve the matching model from the collection.
+			var model = new OCA.Files.FileInfoModel(this.elementToFile($tr), {
+				filesClient: this.filesClient
+			});
+			if (!model.get('path')) {
+				model.set('path', this.getCurrentDirectory(), {silent: true});
+			}
+
+			model.on('change', function(model) {
+				// re-render row
+				var highlightState = $tr.hasClass('highlighted');
+				$tr = self.updateRow(
+					$tr,
+					model.toJSON(),
+					{updateSummary: true, silent: false, animate: true}
+				);
+
+				// restore selection state
+				var selected = !!self._selectedFiles[$tr.data('id')];
+				self._selectFileEl($tr, selected);
+
+				$tr.toggleClass('highlighted', highlightState);
+			});
+			model.on('busy', function(model, state) {
+				self.showFileBusyState($tr, state);
+			});
+
+			return model;
+		},
+
+		/**
+		 * Displays the details view for the given file and
+		 * selects the given tab
+		 *
+		 * @param {string|OCA.Files.FileInfoModel} fileName file name or FileInfoModel for which to show details
+		 * @param {string} [tabId] optional tab id to select
+		 */
+		showDetailsView: function(fileName, tabId) {
+			this._updateDetailsView(fileName);
+			if (tabId) {
+				this._detailsView.selectTab(tabId);
+			}
+			OC.Apps.showAppSidebar(this._detailsView.$el);
+		},
+
+		/**
+		 * Update the details view to display the given file
+		 *
+		 * @param {string|OCA.Files.FileInfoModel} fileName file name from the current list or a FileInfoModel object
+		 * @param {boolean} [show=true] whether to open the sidebar if it was closed
+		 */
+		_updateDetailsView: function(fileName, show) {
+			if (!this._detailsView) {
+				return;
+			}
+
+			// show defaults to true
+			show = _.isUndefined(show) || !!show;
+			var oldFileInfo = this._detailsView.getFileInfo();
+			if (oldFileInfo) {
+				// TODO: use more efficient way, maybe track the highlight
+				this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted');
+				oldFileInfo.off('change', this._onSelectedModelChanged, this);
+			}
+
+			if (!fileName) {
+				this._detailsView.setFileInfo(null);
+				if (this._currentFileModel) {
+					this._currentFileModel.off();
+				}
+				this._currentFileModel = null;
+				OC.Apps.hideAppSidebar(this._detailsView.$el);
+				return;
+			}
+
+			if (show && this._detailsView.$el.hasClass('disappear')) {
+				OC.Apps.showAppSidebar(this._detailsView.$el);
+			}
+
+			if (fileName instanceof OCA.Files.FileInfoModel) {
+				var model = fileName;
+			} else {
+				var $tr = this.findFileEl(fileName);
+				var model = this.getModelForFile($tr);
+				$tr.addClass('highlighted');
+			}
+
+			this._currentFileModel = model;
+
+			this._replaceDetailsViewElementIfNeeded();
+
+			this._detailsView.setFileInfo(model);
+			this._detailsView.$el.scrollTop(0);
+		},
+
+		/**
+		 * Replaces the current details view element with the details view
+		 * element of this file list.
+		 *
+		 * Each file list has its own DetailsView object, and each one has its
+		 * own root element, but there can be just one details view/sidebar
+		 * element in the document. This helper method replaces the current
+		 * details view/sidebar element in the document with the element from
+		 * the DetailsView object of this file list.
+		 */
+		_replaceDetailsViewElementIfNeeded: function() {
+			var $appSidebar = $('#app-sidebar');
+			if ($appSidebar.length === 0) {
+				this._detailsView.$el.insertAfter($('#app-content'));
+			} else if ($appSidebar[0] !== this._detailsView.el) {
+				// "replaceWith()" can not be used here, as it removes the old
+				// element instead of just detaching it.
+				this._detailsView.$el.insertBefore($appSidebar);
+				$appSidebar.detach();
+			}
+		},
+
+		/**
+		 * Event handler for when the window size changed
+		 */
+		_onResize: function() {
+			var containerWidth = this.$el.width();
+			var actionsWidth = 0;
+			$.each(this.$el.find('#controls .actions'), function(index, action) {
+				actionsWidth += $(action).outerWidth();
+			});
+
+			this.breadcrumb._resize();
+		},
+
+		/**
+		 * Toggle showing gridview by default or not
+		 *
+		 * @returns {undefined}
+		 */
+		_onGridviewChange: function() {
+			var show = this.$showGridView.is(':checked');
+			// only save state if user is logged in
+			if (OC.currentUser) {
+				$.post(OC.generateUrl('/apps/files/api/v1/showgridview'), {
+					show: show
+				});
+			}
+			this.$showGridView.next('#view-toggle')
+				.removeClass('icon-toggle-filelist icon-toggle-pictures')
+				.addClass(show ? 'icon-toggle-filelist' : 'icon-toggle-pictures')
+				
+			$('.list-container').toggleClass('view-grid', show);
+			if (show) {
+				// If switching into grid view from list view, too few files might be displayed
+				// Try rendering the next page
+				this._onScroll();
+			}
+		},
+
+		/**
+		 * Event handler when leaving previously hidden state
+		 */
+		_onShow: function(e) {
+			if (this.shown) {
+				if (e.itemId === this.id) {
+					this._setCurrentDir('/', false);
+				}
+				// Only reload if we don't navigate to a different directory
+				if (typeof e.dir === 'undefined' || e.dir === this.getCurrentDirectory()) {
+					this.reload();
+				}
+			}
+			this.shown = true;
+		},
+
+		/**
+		 * Event handler for when the URL changed
+		 */
+		_onUrlChanged: function(e) {
+			if (e && _.isString(e.dir)) {
+				var currentDir = this.getCurrentDirectory();
+				// this._currentDirectory is NULL when fileList is first initialised
+				if( (this._currentDirectory || this.$el.find('#dir').val()) && currentDir === e.dir) {
+					return;
+				}
+				this.changeDirectory(e.dir, false, true);
+			}
+		},
+
+		/**
+		 * Selected/deselects the given file element and updated
+		 * the internal selection cache.
+		 *
+		 * @param {Object} $tr single file row element
+		 * @param {bool} state true to select, false to deselect
+		 */
+		_selectFileEl: function($tr, state) {
+			var $checkbox = $tr.find('td.selection>.selectCheckBox');
+			var oldData = !!this._selectedFiles[$tr.data('id')];
+			var data;
+			$checkbox.prop('checked', state);
+			$tr.toggleClass('selected', state);
+			// already selected ?
+			if (state === oldData) {
+				return;
+			}
+			data = this.elementToFile($tr);
+			if (state) {
+				this._selectedFiles[$tr.data('id')] = data;
+				this._selectionSummary.add(data);
+			}
+			else {
+				delete this._selectedFiles[$tr.data('id')];
+				this._selectionSummary.remove(data);
+			}
+			if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
+				// hide sidebar
+				this._updateDetailsView(null);
+			}
+			this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
+		},
+
+		_selectRange: function($tr) {
+			var checked = $tr.hasClass('selected');
+			var $lastTr = $(this._lastChecked);
+			var lastIndex = $lastTr.index();
+			var currentIndex = $tr.index();
+			var $rows = this.$fileList.children('tr');
+
+			// last clicked checkbox below current one ?
+			if (lastIndex > currentIndex) {
+				var aux = lastIndex;
+				lastIndex = currentIndex;
+				currentIndex = aux;
+			}
+
+			// auto-select everything in-between
+			for (var i = lastIndex; i <= currentIndex; i++) {
+				this._selectFileEl($rows.eq(i), !checked);
+			}
+			this._removeHalfSelection();
+			this._selectionMode = 'single';
+		},
+
+		_selectSingle: function($tr) {
+			var state = !$tr.hasClass('selected');
+			this._selectFileEl($tr, state);
+		},
+
+		_onMouseOverCheckbox: function(e) {
+			if (this._getCurrentSelectionMode() !== 'range') {
+				return;
+			}
+			var $currentTr = $(e.target).closest('tr');
+
+			var $lastTr = $(this._lastChecked);
+			var lastIndex = $lastTr.index();
+			var currentIndex = $currentTr.index();
+			var $rows = this.$fileList.children('tr');
+
+			// last clicked checkbox below current one ?
+			if (lastIndex > currentIndex) {
+				var aux = lastIndex;
+				lastIndex = currentIndex;
+				currentIndex = aux;
+			}
+
+			// auto-select everything in-between
+			this._removeHalfSelection();
+			for (var i = 0; i <= $rows.length; i++) {
+				var $tr = $rows.eq(i);
+				var $checkbox = $tr.find('td.selection>.selectCheckBox');
+				if(lastIndex <= i && i <= currentIndex) {
+					$tr.addClass('halfselected');
+					$checkbox.prop('checked', true);
+				}
+			}
+		},
+
+		_removeHalfSelection: function() {
+			var $rows = this.$fileList.children('tr');
+			for (var i = 0; i <= $rows.length; i++) {
+				var $tr = $rows.eq(i);
+				$tr.removeClass('halfselected');
+				var $checkbox = $tr.find('td.selection>.selectCheckBox');
+				$checkbox.prop('checked', !!this._selectedFiles[$tr.data('id')]);
+			}
+		},
+
+		/**
+		 * Event handler for when clicking on files to select them
+		 */
+		_onClickFile: function(event) {
+			var $tr = $(event.target).closest('tr');
+			if ($tr.hasClass('dragging')) {
+				return;
+			}
+			if (this._allowSelection && (event.ctrlKey || event.shiftKey)) {
+				event.preventDefault();
+				if (event.shiftKey) {
+					this._selectRange($tr);
+				} else {
+					this._selectSingle($tr);
+				}
+				this._lastChecked = $tr;
+				this.updateSelectionSummary();
+			} else {
+				// clicked directly on the name
+				if (!this._detailsView || $(event.target).is('.nametext, .name, .thumbnail') || $(event.target).closest('.nametext').length) {
+					var filename = $tr.attr('data-file');
+					var renaming = $tr.data('renaming');
+					if (!renaming) {
+						this.fileActions.currentFile = $tr.find('td');
+						var mime = this.fileActions.getCurrentMimeType();
+						var type = this.fileActions.getCurrentType();
+						var permissions = this.fileActions.getCurrentPermissions();
+						var action = this.fileActions.getDefault(mime,type, permissions);
+						if (action) {
+							event.preventDefault();
+							// also set on global object for legacy apps
+							window.FileActions.currentFile = this.fileActions.currentFile;
+							action(filename, {
+								$file: $tr,
+								fileList: this,
+								fileActions: this.fileActions,
+								dir: $tr.attr('data-path') || this.getCurrentDirectory()
+							});
+						}
+						// deselect row
+						$(event.target).closest('a').blur();
+					}
+				} else {
+					// Even if there is no Details action the default event
+					// handler is prevented for consistency (although there
+					// should always be a Details action); otherwise the link
+					// would be downloaded by the browser when the user expected
+					// the details to be shown.
+					event.preventDefault();
+					var filename = $tr.attr('data-file');
+					this.fileActions.currentFile = $tr.find('td');
+					var mime = this.fileActions.getCurrentMimeType();
+					var type = this.fileActions.getCurrentType();
+					var permissions = this.fileActions.getCurrentPermissions();
+					var action = this.fileActions.get(mime, type, permissions)['Details'];
+					if (action) {
+						// also set on global object for legacy apps
+						window.FileActions.currentFile = this.fileActions.currentFile;
+						action(filename, {
+							$file: $tr,
+							fileList: this,
+							fileActions: this.fileActions,
+							dir: $tr.attr('data-path') || this.getCurrentDirectory()
+						});
+					}
+				}
+			}
+		},
+
+		/**
+		 * Event handler for when clicking on a file's checkbox
+		 */
+		_onClickFileCheckbox: function(e) {
+			var $tr = $(e.target).closest('tr');
+			if(this._getCurrentSelectionMode() === 'range') {
+				this._selectRange($tr);
+			} else {
+				this._selectSingle($tr);
+			}
+			this._lastChecked = $tr;
+			this.updateSelectionSummary();
+			if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
+				// hide sidebar
+				this._updateDetailsView(null);
+			}
+		},
+
+		/**
+		 * Event handler for when selecting/deselecting all files
+		 */
+		_onClickSelectAll: function(e) {
+			var hiddenFiles = this.$fileList.find('tr.hidden');
+			var checked = e.target.checked;
+
+			if (hiddenFiles.length > 0) {
+				// set indeterminate alongside checked
+				e.target.indeterminate = checked;
+			} else {
+				e.target.indeterminate = false
+			}
+
+			// Select only visible checkboxes to filter out unmatched file in search
+			this.$fileList.find('td.selection > .selectCheckBox:visible').prop('checked', checked)
+				.closest('tr').toggleClass('selected', checked);
+
+			if (checked) {
+				for (var i = 0; i < this.files.length; i++) {
+					// a search will automatically hide the unwanted rows
+					// let's only select the matches
+					var fileData = this.files[i];
+					var fileRow = this.$fileList.find('tr[data-id=' + fileData.id + ']');
+					// do not select already selected ones
+					if (!fileRow.hasClass('hidden') && _.isUndefined(this._selectedFiles[fileData.id])) {
+						this._selectedFiles[fileData.id] = fileData;
+						this._selectionSummary.add(fileData);
+					}
+				}
+			} else {
+				// if we have some hidden row, then we're in a search
+				// Let's only deselect the visible ones
+				if (hiddenFiles.length > 0) {
+					var visibleFiles = this.$fileList.find('tr:not(.hidden)');
+					var self = this;
+					visibleFiles.each(function() {
+						var id = parseInt($(this).data('id'));
+						// do not deselect already deselected ones
+						if (!_.isUndefined(self._selectedFiles[id])) {
+							// a search will automatically hide the unwanted rows
+							// let's only select the matches
+							var fileData = self._selectedFiles[id];
+							delete self._selectedFiles[fileData.id];
+							self._selectionSummary.remove(fileData);
+						}
+					});
+				} else {
+					this._selectedFiles = {};
+					this._selectionSummary.clear();
+				}
+			}
+			this.updateSelectionSummary();
+			if (this._detailsView && !this._detailsView.$el.hasClass('disappear')) {
+				// hide sidebar
+				this._updateDetailsView(null);
+			}
+		},
+
+		/**
+		 * Event handler for when clicking on "Download" for the selected files
+		 */
+		_onClickDownloadSelected: function(event) {
+			var files;
+			var self = this;
+			var dir = this.getCurrentDirectory();
+
+			if (this.isAllSelected() && this.getSelectedFiles().length > 1) {
+				files = OC.basename(dir);
+				dir = OC.dirname(dir) || '/';
+			}
+			else {
+				files = _.pluck(this.getSelectedFiles(), 'name');
+			}
+
+			// don't allow a second click on the download action
+			if(this.fileMultiSelectMenu.isDisabled('download')) {
+				return false;
+			}
+
+			this.fileMultiSelectMenu.toggleLoading('download', true);
+			var disableLoadingState = function(){
+				self.fileMultiSelectMenu.toggleLoading('download', false);
+			};
+
+			if(this.getSelectedFiles().length > 1) {
+				OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir, true), disableLoadingState);
+			}
+			else {
+				var first = this.getSelectedFiles()[0];
+				OCA.Files.Files.handleDownload(this.getDownloadUrl(first.name, dir, true), disableLoadingState);
+			}
+			event.preventDefault();
+		},
+
+		/**
+		 * Event handler for when clicking on "Move" for the selected files
+		 */
+		_onClickCopyMoveSelected: function(event) {
+			var files;
+			var self = this;
+
+			files = _.pluck(this.getSelectedFiles(), 'name');
+
+			// don't allow a second click on the download action
+			if(this.fileMultiSelectMenu.isDisabled('copyMove')) {
+				return false;
+			}
+
+			var disableLoadingState = function(){
+				self.fileMultiSelectMenu.toggleLoading('copyMove', false);
+			};
+
+			var actions = this.isSelectedMovable() ? OC.dialogs.FILEPICKER_TYPE_COPY_MOVE : OC.dialogs.FILEPICKER_TYPE_COPY;
+			var dialogDir = self.getCurrentDirectory();
+			if (typeof self.dirInfo.dirLastCopiedTo !== 'undefined') {
+				dialogDir = self.dirInfo.dirLastCopiedTo;
+			}
+			OC.dialogs.filepicker(t('files', 'Choose target folder'), function(targetPath, type) {
+				self.fileMultiSelectMenu.toggleLoading('copyMove', true);
+				if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
+					self.copy(files, targetPath, disableLoadingState);
+				}
+				if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
+					self.move(files, targetPath, disableLoadingState);
+				}
+				self.dirInfo.dirLastCopiedTo = targetPath; 
+			}, false, "httpd/unix-directory", true, actions, dialogDir);
+			event.preventDefault();
+		},
+
+		/**
+		 * Event handler for when clicking on "Delete" for the selected files
+		 */
+		_onClickDeleteSelected: function(event) {
+			var files = null;
+			if (!this.isAllSelected()) {
+				files = _.pluck(this.getSelectedFiles(), 'name');
+			}
+			this.do_delete(files);
+			event.preventDefault();
+		},
+
+		/**
+		 * Event handler when clicking on a table header
+		 */
+		_onClickHeader: function(e) {
+			if (this.$table.hasClass('multiselect')) {
+				return;
+			}
+			var $target = $(e.target);
+			var sort;
+			if (!$target.is('a')) {
+				$target = $target.closest('a');
+			}
+			sort = $target.attr('data-sort');
+			if (sort && this._allowSorting) {
+				if (this._sort === sort) {
+					this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc', true, true);
+				}
+				else {
+					if ( sort === 'name' ) {	//default sorting of name is opposite to size and mtime
+						this.setSort(sort, 'asc', true, true);
+					}
+					else {
+						this.setSort(sort, 'desc', true, true);
+					}
+				}
+			}
+		},
+
+		/**
+		 * Event handler when clicking on a bread crumb
+		 */
+		_onClickBreadCrumb: function(e) {
+			// Select a crumb or a crumb in the menu
+			var $el = $(e.target).closest('.crumb, .crumblist'),
+				$targetDir = $el.data('dir');
+
+			if ($targetDir !== undefined && e.which === 1) {
+				e.preventDefault();
+				this.changeDirectory($targetDir, true, true);
+				this.updateSearch();
+			}
+		},
+
+		/**
+		 * Event handler for when scrolling the list container.
+		 * This appends/renders the next page of entries when reaching the bottom.
+		 */
+		_onScroll: function(e) {
+			if (this.$container.scrollTop() + this.$container.height() > this.$el.height() - 300) {
+				this._nextPage(true);
+			}
+		},
+
+		/**
+		 * Event handler when dropping on a breadcrumb
+		 */
+		_onDropOnBreadCrumb: function( event, ui ) {
+			var self = this;
+			var $target = $(event.target);
+			if (!$target.is('.crumb, .crumblist')) {
+				$target = $target.closest('.crumb, .crumblist');
+			}
+			var targetPath = $(event.target).data('dir');
+			var dir = this.getCurrentDirectory();
+			while (dir.substr(0,1) === '/') {//remove extra leading /'s
+				dir = dir.substr(1);
+			}
+			dir = '/' + dir;
+			if (dir.substr(-1,1) !== '/') {
+				dir = dir + '/';
+			}
+			// do nothing if dragged on current dir
+			if (targetPath === dir || targetPath + '/' === dir) {
+				return;
+			}
+
+			var files = this.getSelectedFiles();
+			if (files.length === 0) {
+				// single one selected without checkbox?
+				files = _.map(ui.helper.find('tr'), function(el) {
+					return self.elementToFile($(el));
+				});
+			}
+
+			var movePromise = this.move(_.pluck(files, 'name'), targetPath);
+
+			// re-enable td elements to be droppable
+			// sometimes the filename drop handler is still called after re-enable,
+			// it seems that waiting for a short time before re-enabling solves the problem
+			setTimeout(function() {
+				self.$el.find('td.filename.ui-droppable').droppable('enable');
+			}, 10);
+
+			return movePromise;
+		},
+
+		/**
+		 * Sets a new page title
+		 */
+		setPageTitle: function(title){
+			if (title) {
+				title += ' - ';
+			} else {
+				title = '';
+			}
+			title += this.appName;
+			// Sets the page title with the " - Nextcloud" suffix as in templates
+			window.document.title = title + ' - ' + OC.theme.title;
+
+			return true;
+		},
+		/**
+		 * Returns the file info for the given file name from the internal collection.
+		 *
+		 * @param {string} fileName file name
+		 * @return {OCA.Files.FileInfo} file info or null if it was not found
+		 *
+		 * @since 8.2
+		 */
+		findFile: function(fileName) {
+			return _.find(this.files, function(aFile) {
+				return (aFile.name === fileName);
+			}) || null;
+		},
+		/**
+		 * Returns the tr element for a given file name, but only if it was already rendered.
+		 *
+		 * @param {string} fileName file name
+		 * @return {Object} jQuery object of the matching row
+		 */
+		findFileEl: function(fileName){
+			// use filterAttr to avoid escaping issues
+			return this.$fileList.find('tr').filterAttr('data-file', fileName);
+		},
+
+		/**
+		 * Returns the file data from a given file element.
+		 * @param $el file tr element
+		 * @return file data
+		 */
+		elementToFile: function($el){
+			$el = $($el);
+			var data = {
+				id: parseInt($el.attr('data-id'), 10),
+				name: $el.attr('data-file'),
+				mimetype: $el.attr('data-mime'),
+				mtime: parseInt($el.attr('data-mtime'), 10),
+				type: $el.attr('data-type'),
+				etag: $el.attr('data-etag'),
+				permissions: parseInt($el.attr('data-permissions'), 10),
+				hasPreview: $el.attr('data-has-preview') === 'true',
+				isEncrypted: $el.attr('data-e2eencrypted') === 'true'
+			};
+			var size = $el.attr('data-size');
+			if (size) {
+				data.size = parseInt(size, 10);
+			}
+			var icon = $el.attr('data-icon');
+			if (icon) {
+				data.icon = icon;
+			}
+			var mountType = $el.attr('data-mounttype');
+			if (mountType) {
+				data.mountType = mountType;
+			}
+			var path = $el.attr('data-path');
+			if (path) {
+				data.path = path;
+			}
+			return data;
+		},
+
+		/**
+		 * Appends the next page of files into the table
+		 * @param animate true to animate the new elements
+		 * @return array of DOM elements of the newly added files
+		 */
+		_nextPage: function(animate) {
+			var index = this.$fileList.children().length,
+				count = this.pageSize(),
+				hidden,
+				tr,
+				fileData,
+				newTrs = [],
+				isAllSelected = this.isAllSelected(),
+				showHidden = this._filesConfig.get('showhidden');
+
+			if (index >= this.files.length) {
+				return false;
+			}
+
+			while (count > 0 && index < this.files.length) {
+				fileData = this.files[index];
+				if (this._filter) {
+					hidden = fileData.name.toLowerCase().indexOf(this._filter.toLowerCase()) === -1;
+				} else {
+					hidden = false;
+				}
+				tr = this._renderRow(fileData, {updateSummary: false, silent: true, hidden: hidden});
+				this.$fileList.append(tr);
+				if (isAllSelected || this._selectedFiles[fileData.id]) {
+					tr.addClass('selected');
+					tr.find('.selectCheckBox').prop('checked', true);
+				}
+				if (animate) {
+					tr.addClass('appear transparent');
+				}
+				newTrs.push(tr);
+				index++;
+				// only count visible rows
+				if (showHidden || !tr.hasClass('hidden-file')) {
+					count--;
+				}
+			}
+
+			// trigger event for newly added rows
+			if (newTrs.length > 0) {
+				this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: newTrs}));
+			}
+
+			if (animate) {
+				// defer, for animation
+				window.setTimeout(function() {
+					for (var i = 0; i < newTrs.length; i++ ) {
+						newTrs[i].removeClass('transparent');
+					}
+				}, 0);
+			}
+
+			return newTrs;
+		},
+
+		/**
+		 * Event handler for when file actions were updated.
+		 * This will refresh the file actions on the list.
+		 */
+		_onFileActionsUpdated: function() {
+			var self = this;
+			var $files = this.$fileList.find('tr');
+			if (!$files.length) {
+				return;
+			}
+
+			$files.each(function() {
+				self.fileActions.display($(this).find('td.filename'), false, self);
+			});
+			this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $files}));
+
+		},
+
+		/**
+		 * Sets the files to be displayed in the list.
+		 * This operation will re-render the list and update the summary.
+		 * @param filesArray array of file data (map)
+		 */
+		setFiles: function(filesArray) {
+			var self = this;
+
+			// detach to make adding multiple rows faster
+			this.files = filesArray;
+
+			this.$fileList.empty();
+
+			if (this._allowSelection) {
+				// The results table, which has no selection column, checks
+				// whether the main table has a selection column or not in order
+				// to align its contents with those of the main table.
+				this.$el.addClass('has-selection');
+			}
+
+			// clear "Select all" checkbox
+			this.$el.find('.select-all').prop('checked', false);
+
+			// Save full files list while rendering
+
+			this.isEmpty = this.files.length === 0;
+			this._nextPage();
+
+			this.updateEmptyContent();
+
+			this.fileSummary.calculate(this.files);
+
+			this._selectedFiles = {};
+			this._selectionSummary.clear();
+			this.updateSelectionSummary();
+			$(window).scrollTop(0);
+
+			this.$fileList.trigger(jQuery.Event('updated'));
+			_.defer(function() {
+				self.$el.closest('#app-content').trigger(jQuery.Event('apprendered'));
+			});
+		},
+
+		/**
+		 * Returns whether the given file info must be hidden
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo file info
+		 *
+		 * @return {boolean} true if the file is a hidden file, false otherwise
+		 */
+		_isHiddenFile: function(file) {
+			return file.name && file.name.charAt(0) === '.';
+		},
+
+		/**
+		 * Returns the icon URL matching the given file info
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo file info
+		 *
+		 * @return {string} icon URL
+		 */
+		_getIconUrl: function(fileInfo) {
+			var mimeType = fileInfo.mimetype || 'application/octet-stream';
+			if (mimeType === 'httpd/unix-directory') {
+				// use default folder icon
+				if (fileInfo.mountType === 'shared' || fileInfo.mountType === 'shared-root') {
+					return OC.MimeType.getIconUrl('dir-shared');
+				} else if (fileInfo.mountType === 'external-root') {
+					return OC.MimeType.getIconUrl('dir-external');
+				} else if (fileInfo.mountType !== undefined && fileInfo.mountType !== '') {
+					return OC.MimeType.getIconUrl('dir-' + fileInfo.mountType);
+				}
+				return OC.MimeType.getIconUrl('dir');
+			}
+			return OC.MimeType.getIconUrl(mimeType);
+		},
+
+		/**
+		 * Creates a new table row element using the given file data.
+		 * @param {OC.Files.FileInfo} fileData file info attributes
+		 * @param options map of attributes
+		 * @return new tr element (not appended to the table)
+		 */
+		_createRow: function(fileData, options) {
+			var td, simpleSize, basename, extension, sizeColor,
+				icon = fileData.icon || this._getIconUrl(fileData),
+				name = fileData.name,
+				// TODO: get rid of type, only use mime type
+				type = fileData.type || 'file',
+				mtime = parseInt(fileData.mtime, 10),
+				mime = fileData.mimetype,
+				path = fileData.path,
+				dataIcon = null,
+				linkUrl;
+			options = options || {};
+
+			if (isNaN(mtime)) {
+				mtime = new Date().getTime();
+			}
+
+			if (type === 'dir') {
+				mime = mime || 'httpd/unix-directory';
+
+				if (fileData.isEncrypted) {
+					icon = OC.MimeType.getIconUrl('dir-encrypted');
+					dataIcon = icon;
+				} else if (fileData.mountType && fileData.mountType.indexOf('external') === 0) {
+					icon = OC.MimeType.getIconUrl('dir-external');
+					dataIcon = icon;
+				}
+			}
+
+			var permissions = fileData.permissions;
+			if (permissions === undefined || permissions === null) {
+				permissions = this.getDirectoryPermissions();
+			}
+
+			//containing tr
+			var tr = $('<tr></tr>').attr({
+				"data-id" : fileData.id,
+				"data-type": type,
+				"data-size": fileData.size,
+				"data-file": name,
+				"data-mime": mime,
+				"data-mtime": mtime,
+				"data-etag": fileData.etag,
+				"data-permissions": permissions,
+				"data-has-preview": fileData.hasPreview !== false,
+				"data-e2eencrypted": fileData.isEncrypted === true
+			});
+
+			if (dataIcon) {
+				// icon override
+				tr.attr('data-icon', dataIcon);
+			}
+
+			if (fileData.mountType) {
+				// dirInfo (parent) only exist for the "real" file list
+				if (this.dirInfo.id) {
+					// FIXME: HACK: detect shared-root
+					if (fileData.mountType === 'shared' && this.dirInfo.mountType !== 'shared' && this.dirInfo.mountType !== 'shared-root') {
+						// if parent folder isn't share, assume the displayed folder is a share root
+						fileData.mountType = 'shared-root';
+					} else if (fileData.mountType === 'external' && this.dirInfo.mountType !== 'external' && this.dirInfo.mountType !== 'external-root') {
+						// if parent folder isn't external, assume the displayed folder is the external storage root
+						fileData.mountType = 'external-root';
+					}
+				}
+				tr.attr('data-mounttype', fileData.mountType);
+			}
+
+			if (!_.isUndefined(path)) {
+				tr.attr('data-path', path);
+			}
+			else {
+				path = this.getCurrentDirectory();
+			}
+
+			// selection td
+			if (this._allowSelection) {
+				td = $('<td class="selection"></td>');
+
+				td.append(
+					'<input id="select-' + this.id + '-' + fileData.id +
+					'" type="checkbox" class="selectCheckBox checkbox"/><label for="select-' + this.id + '-' + fileData.id + '">' +
+					'<span class="hidden-visually">' + t('files', 'Select') + '</span>' +
+					'</label>'
+				);
+
+				tr.append(td);
+			}
+
+			// filename td
+			td = $('<td class="filename"></td>');
+
+
+			// linkUrl
+			if (mime === 'httpd/unix-directory') {
+				linkUrl = this.linkTo(path + '/' + name);
+			}
+			else {
+				linkUrl = this.getDownloadUrl(name, path, type === 'dir');
+			}
+			var linkElem = $('<a></a>').attr({
+				"class": "name",
+				"href": linkUrl
+			});
+
+			linkElem.append('<div class="thumbnail-wrapper"><div class="thumbnail" style="background-image:url(' + icon + ');"></div></div>');
+
+			// from here work on the display name
+			name = fileData.displayName || name;
+
+			// show hidden files (starting with a dot) completely in gray
+			if(name.indexOf('.') === 0) {
+				basename = '';
+				extension = name;
+			// split extension from filename for non dirs
+			} else if (mime !== 'httpd/unix-directory' && name.indexOf('.') !== -1) {
+				basename = name.substr(0, name.lastIndexOf('.'));
+				extension = name.substr(name.lastIndexOf('.'));
+			} else {
+				basename = name;
+				extension = false;
+			}
+			var nameSpan=$('<span></span>').addClass('nametext');
+			var innernameSpan = $('<span></span>').addClass('innernametext').text(basename);
+
+
+			var conflictingItems = this.$fileList.find('tr[data-file="' + this._jqSelEscape(name) + '"]');
+			if (conflictingItems.length !== 0) {
+				if (conflictingItems.length === 1) {
+					// Update the path on the first conflicting item
+					var $firstConflict = $(conflictingItems[0]),
+						firstConflictPath = $firstConflict.attr('data-path') + '/';
+					if (firstConflictPath.charAt(0) === '/') {
+						firstConflictPath = firstConflictPath.substr(1);
+					}
+					if (firstConflictPath && firstConflictPath !== '/') {
+						$firstConflict.find('td.filename span.innernametext').prepend($('<span></span>').addClass('conflict-path').text(firstConflictPath));
+					}
+				}
+
+				var conflictPath = path + '/';
+				if (conflictPath.charAt(0) === '/') {
+					conflictPath = conflictPath.substr(1);
+				}
+				if (path && path !== '/') {
+					nameSpan.append($('<span></span>').addClass('conflict-path').text(conflictPath));
+				}
+			}
+
+			nameSpan.append(innernameSpan);
+			linkElem.append(nameSpan);
+			if (extension) {
+				nameSpan.append($('<span></span>').addClass('extension').text(extension));
+			}
+			if (fileData.extraData) {
+				if (fileData.extraData.charAt(0) === '/') {
+					fileData.extraData = fileData.extraData.substr(1);
+				}
+				nameSpan.addClass('extra-data').attr('title', fileData.extraData);
+				nameSpan.tooltip({placement: 'top'});
+			}
+			// dirs can show the number of uploaded files
+			if (mime === 'httpd/unix-directory') {
+				linkElem.append($('<span></span>').attr({
+					'class': 'uploadtext',
+					'currentUploads': 0
+				}));
+			}
+			td.append(linkElem);
+			tr.append(td);
+
+			try {
+				var maxContrastHex = window.getComputedStyle(document.documentElement)
+					.getPropertyValue('--color-text-maxcontrast').trim()
+				if (maxContrastHex.length < 4) {
+					throw Error();
+				}
+				var maxContrast = parseInt(maxContrastHex.substring(1, 3), 16)
+			} catch(error) {
+				var maxContrast = OCA.Accessibility
+					&& OCA.Accessibility.theme === 'themedark'
+						? 130
+						: 118
+			}
+
+			// size column
+			if (typeof(fileData.size) !== 'undefined' && fileData.size >= 0) {
+				simpleSize = humanFileSize(parseInt(fileData.size, 10), true);
+				// rgb(118, 118, 118) / #767676
+				// min. color contrast for normal text on white background according to WCAG AA
+				sizeColor = Math.round(118-Math.pow((fileData.size/(1024*1024)), 2));
+
+				// ensure that the brightest color is still readable
+				// min. color contrast for normal text on white background according to WCAG AA
+				if (sizeColor >= maxContrast) {
+					sizeColor = maxContrast;
+				}
+
+				if (OCA.Accessibility && OCA.Accessibility.theme === 'themedark') {
+					sizeColor = Math.abs(sizeColor);
+					// ensure that the dimmest color is still readable
+					// min. color contrast for normal text on black background according to WCAG AA
+					if (sizeColor < maxContrast) {
+						sizeColor = maxContrast;
+					}
+				}
+			} else {
+				simpleSize = t('files', 'Pending');
+			}
+
+			td = $('<td></td>').attr({
+				"class": "filesize",
+				"style": 'color:rgb(' + sizeColor + ',' + sizeColor + ',' + sizeColor + ')'
+			}).text(simpleSize);
+			tr.append(td);
+
+			// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
+			// difference in days multiplied by 5 - brightest shade for files older than 32 days (160/5)
+			var modifiedColor = Math.round(((new Date()).getTime() - mtime )/1000/60/60/24*5 );
+
+			// ensure that the brightest color is still readable
+			// min. color contrast for normal text on white background according to WCAG AA
+			if (modifiedColor >= maxContrast) {
+				modifiedColor = maxContrast;
+			}
+
+			if (OCA.Accessibility && OCA.Accessibility.theme === 'themedark') {
+				modifiedColor = Math.abs(modifiedColor);
+
+				// ensure that the dimmest color is still readable
+				// min. color contrast for normal text on black background according to WCAG AA
+				if (modifiedColor < maxContrast) {
+					modifiedColor = maxContrast;
+				}
+			}
+
+			var formatted;
+			var text;
+			if (mtime > 0) {
+				formatted = OC.Util.formatDate(mtime);
+				text = OC.Util.relativeModifiedDate(mtime);
+			} else {
+				formatted = t('files', 'Unable to determine date');
+				text = '?';
+			}
+			td = $('<td></td>').attr({ "class": "date" });
+			td.append($('<span></span>').attr({
+				"class": "modified live-relative-timestamp",
+				"title": formatted,
+				"data-timestamp": mtime,
+				"style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')'
+			}).text(text)
+			  .tooltip({placement: 'top'})
+			);
+			tr.find('.filesize').text(simpleSize);
+			tr.append(td);
+			return tr;
+		},
+
+		/* escape a selector expression for jQuery */
+		_jqSelEscape: function (expression) {
+			if (expression) {
+				return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
+			}
+			return null;
+		},
+
+		/**
+		 * Adds an entry to the files array and also into the DOM
+		 * in a sorted manner.
+		 *
+		 * @param {OC.Files.FileInfo} fileData map of file attributes
+		 * @param {Object} [options] map of attributes
+		 * @param {boolean} [options.updateSummary] true to update the summary
+		 * after adding (default), false otherwise. Defaults to true.
+		 * @param {boolean} [options.silent] true to prevent firing events like "fileActionsReady",
+		 * defaults to false.
+		 * @param {boolean} [options.animate] true to animate the thumbnail image after load
+		 * defaults to true.
+		 * @return new tr element (not appended to the table)
+		 */
+		add: function(fileData, options) {
+			var index;
+			var $tr;
+			var $rows;
+			var $insertionPoint;
+			options = _.extend({animate: true}, options || {});
+
+			// there are three situations to cover:
+			// 1) insertion point is visible on the current page
+			// 2) insertion point is on a not visible page (visible after scrolling)
+			// 3) insertion point is at the end of the list
+
+			$rows = this.$fileList.children();
+			index = this._findInsertionIndex(fileData);
+			if (index > this.files.length) {
+				index = this.files.length;
+			}
+			else {
+				$insertionPoint = $rows.eq(index);
+			}
+
+			// is the insertion point visible ?
+			if ($insertionPoint.length) {
+				// only render if it will really be inserted
+				$tr = this._renderRow(fileData, options);
+				$insertionPoint.before($tr);
+			}
+			else {
+				// if insertion point is after the last visible
+				// entry, append
+				if (index === $rows.length) {
+					$tr = this._renderRow(fileData, options);
+					this.$fileList.append($tr);
+				}
+			}
+
+			this.isEmpty = false;
+			this.files.splice(index, 0, fileData);
+
+			if ($tr && options.animate) {
+				$tr.addClass('appear transparent');
+				window.setTimeout(function() {
+					$tr.removeClass('transparent');
+					$("#fileList tr").removeClass('mouseOver');
+					$tr.addClass('mouseOver');
+				});
+			}
+
+			if (options.scrollTo) {
+				this.scrollTo(fileData.name);
+			}
+
+			// defaults to true if not defined
+			if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
+				this.fileSummary.add(fileData, true);
+				this.updateEmptyContent();
+			}
+
+			return $tr;
+		},
+
+		/**
+		 * Creates a new row element based on the given attributes
+		 * and returns it.
+		 *
+		 * @param {OC.Files.FileInfo} fileData map of file attributes
+		 * @param {Object} [options] map of attributes
+		 * @param {int} [options.index] index at which to insert the element
+		 * @param {boolean} [options.updateSummary] true to update the summary
+		 * after adding (default), false otherwise. Defaults to true.
+		 * @param {boolean} [options.animate] true to animate the thumbnail image after load
+		 * defaults to true.
+		 * @return new tr element (not appended to the table)
+		 */
+		_renderRow: function(fileData, options) {
+			options = options || {};
+			var type = fileData.type || 'file',
+				mime = fileData.mimetype,
+				path = fileData.path || this.getCurrentDirectory(),
+				permissions = parseInt(fileData.permissions, 10) || 0;
+
+			var isEndToEndEncrypted = (type === 'dir' && fileData.isEncrypted);
+
+			if (!isEndToEndEncrypted && fileData.isShareMountPoint) {
+				permissions = permissions | OC.PERMISSION_UPDATE;
+			}
+
+			if (type === 'dir') {
+				mime = mime || 'httpd/unix-directory';
+			}
+			var tr = this._createRow(
+				fileData,
+				options
+			);
+			var filenameTd = tr.find('td.filename');
+
+			// TODO: move dragging to FileActions ?
+			// enable drag only for deletable files
+			if (this._dragOptions && permissions & OC.PERMISSION_DELETE) {
+				filenameTd.draggable(this._dragOptions);
+			}
+			// allow dropping on folders
+			if (this._folderDropOptions && mime === 'httpd/unix-directory') {
+				tr.droppable(this._folderDropOptions);
+			}
+
+			if (options.hidden) {
+				tr.addClass('hidden');
+			}
+
+			if (this._isHiddenFile(fileData)) {
+				tr.addClass('hidden-file');
+			}
+
+			// display actions
+			this.fileActions.display(filenameTd, !options.silent, this);
+
+			if (mime !== 'httpd/unix-directory' && fileData.hasPreview !== false) {
+				var iconDiv = filenameTd.find('.thumbnail');
+				// lazy load / newly inserted td ?
+				// the typeof check ensures that the default value of animate is true
+				if (typeof(options.animate) === 'undefined' || !!options.animate) {
+					this.lazyLoadPreview({
+						fileId: fileData.id,
+						path: path + '/' + fileData.name,
+						mime: mime,
+						etag: fileData.etag,
+						callback: function(url) {
+							iconDiv.css('background-image', 'url("' + url + '")');
+						}
+					});
+				}
+				else {
+					// set the preview URL directly
+					var urlSpec = {
+							file: path + '/' + fileData.name,
+							c: fileData.etag
+						};
+					var previewUrl = this.generatePreviewUrl(urlSpec);
+					previewUrl = previewUrl.replace(/\(/g, '%28').replace(/\)/g, '%29');
+					iconDiv.css('background-image', 'url("' + previewUrl + '")');
+				}
+			}
+			return tr;
+		},
+		/**
+		 * Returns the current directory
+		 * @method getCurrentDirectory
+		 * @return current directory
+		 */
+		getCurrentDirectory: function(){
+			return this._currentDirectory || this.$el.find('#dir').val() || '/';
+		},
+		/**
+		 * Returns the directory permissions
+		 * @return permission value as integer
+		 */
+		getDirectoryPermissions: function() {
+			return this && this.dirInfo && this.dirInfo.permissions ? this.dirInfo.permissions : parseInt(this.$el.find('#permissions').val(), 10);
+		},
+		/**
+		 * Changes the current directory and reload the file list.
+		 * @param {string} targetDir target directory (non URL encoded)
+		 * @param {boolean} [changeUrl=true] if the URL must not be changed (defaults to true)
+		 * @param {boolean} [force=false] set to true to force changing directory
+		 * @param {string} [fileId] optional file id, if known, to be appended in the URL
+		 */
+		changeDirectory: function(targetDir, changeUrl, force, fileId) {
+			var self = this;
+			var currentDir = this.getCurrentDirectory();
+			targetDir = targetDir || '/';
+			if (!force && currentDir === targetDir) {
+				return;
+			}
+			this._setCurrentDir(targetDir, changeUrl, fileId);
+
+			// discard finished uploads list, we'll get it through a regular reload
+			this._uploads = {};
+			return this.reload().then(function(success){
+				if (!success) {
+					self.changeDirectory(currentDir, true);
+				}
+			});
+		},
+		linkTo: function(dir) {
+			return OC.linkTo('files', 'index.php')+"?dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
+		},
+
+		/**
+		 * @param {string} path
+		 * @returns {boolean}
+		 */
+		_isValidPath: function(path) {
+			var sections = path.split('/');
+			for (var i = 0; i < sections.length; i++) {
+				if (sections[i] === '..') {
+					return false;
+				}
+			}
+
+			return path.toLowerCase().indexOf(decodeURI('%0a')) === -1 &&
+				path.toLowerCase().indexOf(decodeURI('%00')) === -1;
+		},
+
+		/**
+		 * Sets the current directory name and updates the breadcrumb.
+		 * @param targetDir directory to display
+		 * @param changeUrl true to also update the URL, false otherwise (default)
+		 * @param {string} [fileId] file id
+		 */
+		_setCurrentDir: function(targetDir, changeUrl, fileId) {
+			targetDir = targetDir.replace(/\\/g, '/');
+			if (!this._isValidPath(targetDir)) {
+				targetDir = '/';
+				changeUrl = true;
+			}
+			var previousDir = this.getCurrentDirectory(),
+				baseDir = OC.basename(targetDir);
+
+			if (baseDir !== '') {
+				this.setPageTitle(baseDir);
+			}
+			else {
+				this.setPageTitle();
+			}
+
+			if (targetDir.length > 0 && targetDir[0] !== '/') {
+				targetDir = '/' + targetDir;
+			}
+			this._currentDirectory = targetDir;
+
+			// legacy stuff
+			this.$el.find('#dir').val(targetDir);
+
+			if (changeUrl !== false) {
+				var params = {
+					dir: targetDir,
+					previousDir: previousDir
+				};
+				if (fileId) {
+					params.fileId = fileId;
+				}
+				this.$el.trigger(jQuery.Event('changeDirectory', params));
+			}
+			this.breadcrumb.setDirectory(this.getCurrentDirectory());
+		},
+		/**
+		 * Sets the current sorting and refreshes the list
+		 *
+		 * @param sort sort attribute name
+		 * @param direction sort direction, one of "asc" or "desc"
+		 * @param update true to update the list, false otherwise (default)
+		 * @param persist true to save changes in the database (default)
+		 */
+		setSort: function(sort, direction, update, persist) {
+			var comparator = FileList.Comparators[sort] || FileList.Comparators.name;
+			this._sort = sort;
+			this._sortDirection = (direction === 'desc')?'desc':'asc';
+			this._sortComparator = function(fileInfo1, fileInfo2) {
+				var isFavorite = function(fileInfo) {
+					return fileInfo.tags && fileInfo.tags.indexOf(OC.TAG_FAVORITE) >= 0;
+				};
+
+				if (isFavorite(fileInfo1) && !isFavorite(fileInfo2)) {
+					return -1;
+				} else if (!isFavorite(fileInfo1) && isFavorite(fileInfo2)) {
+					return 1;
+				}
+
+				return direction === 'asc' ? comparator(fileInfo1, fileInfo2) : -comparator(fileInfo1, fileInfo2);
+			};
+
+			this.$el.find('thead th .sort-indicator')
+				.removeClass(this.SORT_INDICATOR_ASC_CLASS)
+				.removeClass(this.SORT_INDICATOR_DESC_CLASS)
+				.toggleClass('hidden', true)
+				.addClass(this.SORT_INDICATOR_DESC_CLASS);
+
+			this.$el.find('thead th.column-' + sort + ' .sort-indicator')
+				.removeClass(this.SORT_INDICATOR_ASC_CLASS)
+				.removeClass(this.SORT_INDICATOR_DESC_CLASS)
+				.toggleClass('hidden', false)
+				.addClass(direction === 'desc' ? this.SORT_INDICATOR_DESC_CLASS : this.SORT_INDICATOR_ASC_CLASS);
+			if (update) {
+				if (this._clientSideSort) {
+					this.files.sort(this._sortComparator);
+					this.setFiles(this.files);
+				}
+				else {
+					this.reload();
+				}
+			}
+
+			if (persist && OC.getCurrentUser().uid) {
+				$.post(OC.generateUrl('/apps/files/api/v1/sorting'), {
+					mode: sort,
+					direction: direction
+				});
+			}
+		},
+
+		/**
+		 * Returns list of webdav properties to request
+		 */
+		_getWebdavProperties: function() {
+			return [].concat(this.filesClient.getPropfindProperties());
+		},
+
+		/**
+		 * Reloads the file list using ajax call
+		 *
+		 * @return ajax call object
+		 */
+		reload: function() {
+			this._selectedFiles = {};
+			this._selectionSummary.clear();
+			if (this._currentFileModel) {
+				this._currentFileModel.off();
+			}
+			this._currentFileModel = null;
+			this.$el.find('.select-all').prop('checked', false);
+			this.showMask();
+			this._reloadCall = this.filesClient.getFolderContents(
+				this.getCurrentDirectory(), {
+					includeParent: true,
+					properties: this._getWebdavProperties()
+				}
+			);
+			if (this._detailsView) {
+				// close sidebar
+				this._updateDetailsView(null);
+			}
+			this._setCurrentDir(this.getCurrentDirectory(), false);
+			var callBack = this.reloadCallback.bind(this);
+			return this._reloadCall.then(callBack, callBack);
+		},
+		reloadCallback: function(status, result) {
+			delete this._reloadCall;
+			this.hideMask();
+
+			if (status === 401) {
+				return false;
+			}
+
+			// Firewall Blocked request?
+			if (status === 403) {
+				// Go home
+				this.changeDirectory('/');
+				OC.Notification.show(t('files', 'This operation is forbidden'), {type: 'error'});
+				return false;
+			}
+
+			// Did share service die or something else fail?
+			if (status === 500) {
+				// Go home
+				this.changeDirectory('/');
+				OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'),
+					{type: 'error'}
+				);
+				return false;
+			}
+
+			if (status === 503) {
+				// Go home
+				if (this.getCurrentDirectory() !== '/') {
+					this.changeDirectory('/');
+					// TODO: read error message from exception
+					OC.Notification.show(t('files', 'Storage is temporarily not available'),
+						{type: 'error'}
+					);
+				}
+				return false;
+			}
+
+			if (status === 400 || status === 404 || status === 405) {
+				// go back home
+				this.changeDirectory('/');
+				return false;
+			}
+			// aborted ?
+			if (status === 0){
+				return true;
+			}
+
+			this.updateStorageStatistics(true);
+
+			// first entry is the root
+			this.dirInfo = result.shift();
+			this.breadcrumb.setDirectoryInfo(this.dirInfo);
+
+			if (this.dirInfo.permissions) {
+				this._updateDirectoryPermissions();
+			}
+
+			result.sort(this._sortComparator);
+			this.setFiles(result);
+
+			if (this.dirInfo) {
+				var newFileId = this.dirInfo.id;
+				// update fileid in URL
+				var params = {
+					dir: this.getCurrentDirectory()
+				};
+				if (newFileId) {
+					params.fileId = newFileId;
+				}
+				this.$el.trigger(jQuery.Event('afterChangeDirectory', params));
+			}
+			return true;
+		},
+
+		updateStorageStatistics: function(force) {
+			OCA.Files.Files.updateStorageStatistics(this.getCurrentDirectory(), force);
+		},
+
+		updateStorageQuotas: function() {
+			OCA.Files.Files.updateStorageQuotas();
+		},
+
+		/**
+		 * @deprecated do not use nor override
+		 */
+		getAjaxUrl: function(action, params) {
+			return OCA.Files.Files.getAjaxUrl(action, params);
+		},
+
+		getDownloadUrl: function(files, dir, isDir) {
+			return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir);
+		},
+
+		getUploadUrl: function(fileName, dir) {
+			if (_.isUndefined(dir)) {
+				dir = this.getCurrentDirectory();
+			}
+
+			var pathSections = dir.split('/');
+			if (!_.isUndefined(fileName)) {
+				pathSections.push(fileName);
+			}
+			var encodedPath = '';
+			_.each(pathSections, function(section) {
+				if (section !== '') {
+					encodedPath += '/' + encodeURIComponent(section);
+				}
+			});
+			return OC.linkToRemoteBase('webdav') + encodedPath;
+		},
+
+		/**
+		 * Generates a preview URL based on the URL space.
+		 * @param urlSpec attributes for the URL
+		 * @param {int} urlSpec.x width
+		 * @param {int} urlSpec.y height
+		 * @param {String} urlSpec.file path to the file
+		 * @return preview URL
+		 */
+		generatePreviewUrl: function(urlSpec) {
+			urlSpec = urlSpec || {};
+			if (!urlSpec.x) {
+				urlSpec.x = this.$table.data('preview-x') || 250;
+			}
+			if (!urlSpec.y) {
+				urlSpec.y = this.$table.data('preview-y') || 250;
+			}
+			urlSpec.x *= window.devicePixelRatio;
+			urlSpec.y *= window.devicePixelRatio;
+			urlSpec.x = Math.ceil(urlSpec.x);
+			urlSpec.y = Math.ceil(urlSpec.y);
+			urlSpec.forceIcon = 0;
+
+			if (typeof urlSpec.fileId !== 'undefined') {
+				delete urlSpec.file;
+				return OC.generateUrl('/core/preview?') + $.param(urlSpec);
+			} else {
+				delete urlSpec.fileId;
+				return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
+			}
+
+		},
+
+		/**
+		 * Lazy load a file's preview.
+		 *
+		 * @param path path of the file
+		 * @param mime mime type
+		 * @param callback callback function to call when the image was loaded
+		 * @param etag file etag (for caching)
+		 */
+		lazyLoadPreview : function(options) {
+			var self = this;
+			var fileId = options.fileId;
+			var path = options.path;
+			var mime = options.mime;
+			var ready = options.callback;
+			var etag = options.etag;
+
+			// get mime icon url
+			var iconURL = OC.MimeType.getIconUrl(mime);
+			var previewURL,
+				urlSpec = {};
+			ready(iconURL); // set mimeicon URL
+
+			urlSpec.fileId = fileId;
+			urlSpec.file = OCA.Files.Files.fixPath(path);
+			if (options.x) {
+				urlSpec.x = options.x;
+			}
+			if (options.y) {
+				urlSpec.y = options.y;
+			}
+			if (options.a) {
+				urlSpec.a = options.a;
+			}
+			if (options.mode) {
+				urlSpec.mode = options.mode;
+			}
+
+			if (etag){
+				// use etag as cache buster
+				urlSpec.c = etag;
+			}
+
+			previewURL = self.generatePreviewUrl(urlSpec);
+			previewURL = previewURL.replace(/\(/g, '%28').replace(/\)/g, '%29');
+
+			// preload image to prevent delay
+			// this will make the browser cache the image
+			var img = new Image();
+			img.onload = function(){
+				// if loading the preview image failed (no preview for the mimetype) then img.width will < 5
+				if (img.width > 5) {
+					ready(previewURL, img);
+				} else if (options.error) {
+					options.error();
+				}
+			};
+			if (options.error) {
+				img.onerror = options.error;
+			}
+			img.src = previewURL;
+		},
+
+		_updateDirectoryPermissions: function() {
+			var isCreatable = (this.dirInfo.permissions & OC.PERMISSION_CREATE) !== 0 && this.$el.find('#free_space').val() !== '0';
+			this.$el.find('#permissions').val(this.dirInfo.permissions);
+			this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
+			this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
+		},
+		/**
+		 * Shows/hides action buttons
+		 *
+		 * @param show true for enabling, false for disabling
+		 */
+		showActions: function(show){
+			this.$el.find('.actions,#file_action_panel').toggleClass('hidden', !show);
+			if (show){
+				// make sure to display according to permissions
+				var permissions = this.getDirectoryPermissions();
+				var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
+				this.$el.find('.creatable').toggleClass('hidden', !isCreatable);
+				this.$el.find('.notCreatable').toggleClass('hidden', isCreatable);
+				// remove old style breadcrumbs (some apps might create them)
+				this.$el.find('#controls .crumb').remove();
+				// refresh breadcrumbs in case it was replaced by an app
+				this.breadcrumb.render();
+			}
+			else{
+				this.$el.find('.creatable, .notCreatable').addClass('hidden');
+			}
+		},
+		/**
+		 * Enables/disables viewer mode.
+		 * In viewer mode, apps can embed themselves under the controls bar.
+		 * In viewer mode, the actions of the file list will be hidden.
+		 * @param show true for enabling, false for disabling
+		 */
+		setViewerMode: function(show){
+			this.showActions(!show);
+			this.$el.find('#filestable').toggleClass('hidden', show);
+			this.$el.trigger(new $.Event('changeViewerMode', {viewerModeEnabled: show}));
+		},
+		/**
+		 * Removes a file entry from the list
+		 * @param name name of the file to remove
+		 * @param {Object} [options] map of attributes
+		 * @param {boolean} [options.updateSummary] true to update the summary
+		 * after removing, false otherwise. Defaults to true.
+		 * @return deleted element
+		 */
+		remove: function(name, options){
+			options = options || {};
+			var fileEl = this.findFileEl(name);
+			var fileData = _.findWhere(this.files, {name: name});
+			if (!fileData) {
+				return;
+			}
+			var fileId = fileData.id;
+			if (this._selectedFiles[fileId]) {
+				// remove from selection first
+				this._selectFileEl(fileEl, false);
+				this.updateSelectionSummary();
+			}
+			if (this._selectedFiles[fileId]) {
+				delete this._selectedFiles[fileId];
+				this._selectionSummary.remove(fileData);
+				this.updateSelectionSummary();
+			}
+			var index = this.files.findIndex(function(el){return el.name==name;});
+			this.files.splice(index, 1);
+
+			// TODO: improve performance on batch update
+			this.isEmpty = !this.files.length;
+			if (typeof(options.updateSummary) === 'undefined' || !!options.updateSummary) {
+				this.updateEmptyContent();
+				this.fileSummary.remove({type: fileData.type, size: fileData.size}, true);
+			}
+
+			if (!fileEl.length) {
+				return null;
+			}
+
+			if (this._dragOptions && (fileEl.data('permissions') & OC.PERMISSION_DELETE)) {
+				// file is only draggable when delete permissions are set
+				fileEl.find('td.filename').draggable('destroy');
+			}
+			if (this._currentFileModel && this._currentFileModel.get('id') === fileId) {
+				// Note: in the future we should call destroy() directly on the model
+				// and the model will take care of the deletion.
+				// Here we only trigger the event to notify listeners that
+				// the file was removed.
+				this._currentFileModel.trigger('destroy');
+				this._updateDetailsView(null);
+			}
+			fileEl.remove();
+
+			var lastIndex = this.$fileList.children().length;
+			// if there are less elements visible than one page
+			// but there are still pending elements in the array,
+			// then directly append the next page
+			if (lastIndex < this.files.length && lastIndex < this.pageSize()) {
+				this._nextPage(true);
+			}
+
+			return fileEl;
+		},
+		/**
+		 * Finds the index of the row before which the given
+		 * fileData should be inserted, considering the current
+		 * sorting
+		 *
+		 * @param {OC.Files.FileInfo} fileData file info
+		 */
+		_findInsertionIndex: function(fileData) {
+			var index = 0;
+			while (index < this.files.length && this._sortComparator(fileData, this.files[index]) > 0) {
+				index++;
+			}
+			return index;
+		},
+
+		/**
+		 * Moves a file to a given target folder.
+		 *
+		 * @param fileNames array of file names to move
+		 * @param targetPath absolute target path
+		 * @param callback function to call when movement is finished
+		 * @param dir the dir path where fileNames are located (optionnal, will take current folder if undefined)
+		 */
+		move: function(fileNames, targetPath, callback, dir) {
+			var self = this;
+
+			dir = typeof dir === 'string' ? dir : this.getCurrentDirectory();
+			if (dir.charAt(dir.length - 1) !== '/') {
+				dir += '/';
+			}
+			var target = OC.basename(targetPath);
+			if (!_.isArray(fileNames)) {
+				fileNames = [fileNames];
+			}
+
+			var moveFileFunction = function(fileName) {
+				var $tr = self.findFileEl(fileName);
+				self.showFileBusyState($tr, true);
+				if (targetPath.charAt(targetPath.length - 1) !== '/') {
+					// make sure we move the files into the target dir,
+					// not overwrite it
+					targetPath = targetPath + '/';
+				}
+				return self.filesClient.move(dir + fileName, targetPath + fileName)
+					.done(function() {
+						// if still viewing the same directory
+						if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
+							// recalculate folder size
+							var oldFile = self.findFileEl(target);
+							var newFile = self.findFileEl(fileName);
+							var oldSize = oldFile.data('size');
+							var newSize = oldSize + newFile.data('size');
+							oldFile.data('size', newSize);
+							oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
+
+							self.remove(fileName);
+						}
+					})
+					.fail(function(status) {
+						if (status === 412) {
+							// TODO: some day here we should invoke the conflict dialog
+							OC.Notification.show(t('files', 'Could not move "{file}", target exists',
+								{file: fileName}), {type: 'error'}
+							);
+						} else {
+							OC.Notification.show(t('files', 'Could not move "{file}"',
+								{file: fileName}), {type: 'error'}
+							);
+						}
+					})
+					.always(function() {
+						self.showFileBusyState($tr, false);
+					});
+			};
+			return this.reportOperationProgress(fileNames, moveFileFunction, callback);
+		},
+
+		_reflect: function (promise){
+			return promise.then(function(v){ return {};}, function(e){ return {};});
+		},
+
+		reportOperationProgress: function (fileNames, operationFunction, callback){
+			var self = this;
+			self._operationProgressBar.showProgressBar(false);
+			var mcSemaphore = new OCA.Files.Semaphore(5);
+			var counter = 0;
+			var promises = _.map(fileNames, function(arg) {
+				return mcSemaphore.acquire().then(function(){
+					return operationFunction(arg).always(function(){
+						mcSemaphore.release();
+						counter++;
+						self._operationProgressBar.setProgressBarValue(100.0*counter/fileNames.length);
+					});
+				});
+			});
+
+			return Promise.all(_.map(promises, self._reflect)).then(function(){
+				if (callback) {
+					callback();
+				}
+				self._operationProgressBar.hideProgressBar();
+			});
+		},
+
+		/**
+		 * Copies a file to a given target folder.
+		 *
+		 * @param fileNames array of file names to copy
+		 * @param targetPath absolute target path
+		 * @param callback to call when copy is finished with success
+		 * @param dir the dir path where fileNames are located (optionnal, will take current folder if undefined)
+		 */
+		copy: function(fileNames, targetPath, callback, dir) {
+			var self = this;
+			var filesToNotify = [];
+			var count = 0;
+
+			dir = typeof dir === 'string' ? dir : this.getCurrentDirectory();
+			if (dir.charAt(dir.length - 1) !== '/') {
+				dir += '/';
+			}
+			var target = OC.basename(targetPath);
+			if (!_.isArray(fileNames)) {
+				fileNames = [fileNames];
+			}
+			var copyFileFunction = function(fileName) {
+				var $tr = self.findFileEl(fileName);
+				self.showFileBusyState($tr, true);
+				if (targetPath.charAt(targetPath.length - 1) !== '/') {
+					// make sure we move the files into the target dir,
+					// not overwrite it
+					targetPath = targetPath + '/';
+				}
+				var targetPathAndName = targetPath + fileName;
+				if ((dir + fileName) === targetPathAndName) {
+					var dotIndex = targetPathAndName.indexOf(".");
+					if ( dotIndex > 1) {
+						var leftPartOfName = targetPathAndName.substr(0, dotIndex);
+						var fileNumber = leftPartOfName.match(/\d+/);
+						// TRANSLATORS name that is appended to copied files with the same name, will be put in parenthesis and appened with a number if it is the second+ copy
+						var copyNameLocalized = t('files', 'copy');
+						if (isNaN(fileNumber) ) {
+							fileNumber++;
+							targetPathAndName = targetPathAndName.replace(/(?=\.[^.]+$)/g, " (" + copyNameLocalized + " " + fileNumber + ")");
+						}
+						else {
+							// Check if we have other files with 'copy X' and the same name
+							var maxNum = 1;
+							if (self.files !== null) {
+								leftPartOfName = leftPartOfName.replace("/", "");
+								leftPartOfName = leftPartOfName.replace(new RegExp("\\(" + copyNameLocalized + "( \\d+)?\\)"),"");
+								// find the last file with the number extension and add one to the new name
+								for (var j = 0; j < self.files.length; j++) {
+									var cName = self.files[j].name;
+									if (cName.indexOf(leftPartOfName) > -1) {
+										if (cName.indexOf("(" + copyNameLocalized + ")") > 0) {
+											targetPathAndName = targetPathAndName.replace(new RegExp(" \\(" + copyNameLocalized + "\\)"),"");
+											if (maxNum == 1) {
+												maxNum = 2;
+											}
+										}
+										else {
+											var cFileNumber = cName.match(new RegExp("\\(" + copyNameLocalized + " (\\d+)\\)"));
+											if (cFileNumber && parseInt(cFileNumber[1]) >= maxNum) {
+												maxNum = parseInt(cFileNumber[1]) + 1;
+											}
+										}
+									}
+								}
+								targetPathAndName = targetPathAndName.replace(new RegExp(" \\(" + copyNameLocalized + " \\d+\\)"),"");
+							}
+							// Create the new file name with _x at the end
+							// Start from 2 per a special request of the 'standard'
+							var extensionName = " (" + copyNameLocalized + " " + maxNum +")";
+							if (maxNum == 1) {
+								extensionName = " (" + copyNameLocalized + ")";
+							}
+							targetPathAndName = targetPathAndName.replace(/(?=\.[^.]+$)/g, extensionName);
+						}
+					}
+				}
+				return self.filesClient.copy(dir + fileName, targetPathAndName)
+					.done(function () {
+						filesToNotify.push(fileName);
+
+						// if still viewing the same directory
+						if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
+							// recalculate folder size
+							var oldFile = self.findFileEl(target);
+							var newFile = self.findFileEl(fileName);
+							var oldSize = oldFile.data('size');
+							var newSize = oldSize + newFile.data('size');
+							oldFile.data('size', newSize);
+							oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
+						}
+						self.reload();
+					})
+					.fail(function(status) {
+						if (status === 412) {
+							// TODO: some day here we should invoke the conflict dialog
+							OC.Notification.show(t('files', 'Could not copy "{file}", target exists',
+								{file: fileName}), {type: 'error'}
+							);
+						} else {
+							OC.Notification.show(t('files', 'Could not copy "{file}"',
+								{file: fileName}), {type: 'error'}
+							);
+						}
+					})
+					.always(function() {
+						self.showFileBusyState($tr, false);
+						count++;
+
+						/**
+						 * We only show the notifications once the last file has been copied
+						 */
+						if (count === fileNames.length) {
+							// Remove leading and ending /
+							if (targetPath.slice(0, 1) === '/') {
+								targetPath = targetPath.slice(1, targetPath.length);
+							}
+							if (targetPath.slice(-1) === '/') {
+								targetPath = targetPath.slice(0, -1);
+							}
+
+							if (filesToNotify.length > 0) {
+								// Since there's no visual indication that the files were copied, let's send some notifications !
+								if (filesToNotify.length === 1) {
+									OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
+										{
+											origin: filesToNotify[0],
+											destination: targetPath
+										}
+									), {timeout: 10});
+								} else if (filesToNotify.length > 0 && filesToNotify.length < 3) {
+									OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
+										{
+											origin: filesToNotify.join(', '),
+											destination: targetPath
+										}
+									), {timeout: 10});
+								} else {
+									OC.Notification.show(t('files', 'Copied {origin} and {nbfiles} other files inside {destination}',
+										{
+											origin: filesToNotify[0],
+											nbfiles: filesToNotify.length - 1,
+											destination: targetPath
+										}
+									), {timeout: 10});
+								}
+							}
+						}
+					});
+			};
+			return this.reportOperationProgress(fileNames, copyFileFunction, callback);
+		},
+
+		/**
+		 * Updates the given row with the given file info
+		 *
+		 * @param {Object} $tr row element
+		 * @param {OCA.Files.FileInfo} fileInfo file info
+		 * @param {Object} options options
+		 *
+		 * @return {Object} new row element
+		 */
+		updateRow: function($tr, fileInfo, options) {
+			this.files.splice($tr.index(), 1);
+			$tr.remove();
+			options = _.extend({silent: true}, options);
+			options = _.extend(options, {updateSummary: false});
+			$tr = this.add(fileInfo, options);
+			this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $tr}));
+			return $tr;
+		},
+
+		/**
+		 * Triggers file rename input field for the given file name.
+		 * If the user enters a new name, the file will be renamed.
+		 *
+		 * @param oldName file name of the file to rename
+		 */
+		rename: function(oldName) {
+			var self = this;
+			var tr, td, input, form;
+			tr = this.findFileEl(oldName);
+			var oldFileInfo = this.files[tr.index()];
+			tr.data('renaming',true);
+			td = tr.children('td.filename');
+			input = $('<input type="text" class="filename"/>').val(oldName);
+			form = $('<form></form>');
+			form.append(input);
+			td.children('a.name').children(':not(.thumbnail-wrapper)').hide();
+			td.append(form);
+			input.focus();
+			//preselect input
+			var len = input.val().lastIndexOf('.');
+			if ( len === -1 ||
+				tr.data('type') === 'dir' ) {
+				len = input.val().length;
+			}
+			input.selectRange(0, len);
+			var checkInput = function () {
+				var filename = input.val();
+				if (filename !== oldName) {
+					// Files.isFileNameValid(filename) throws an exception itself
+					OCA.Files.Files.isFileNameValid(filename);
+					if (self.inList(filename)) {
+						throw t('files', '{newName} already exists', {newName: filename}, undefined, {
+							escape: false
+						});
+					}
+				}
+				return true;
+			};
+
+			function restore() {
+				input.tooltip('hide');
+				tr.data('renaming',false);
+				form.remove();
+				td.children('a.name').children(':not(.thumbnail-wrapper)').show();
+			}
+
+			function updateInList(fileInfo) {
+				self.updateRow(tr, fileInfo);
+				self._updateDetailsView(fileInfo.name, false);
+			}
+
+			// TODO: too many nested blocks, move parts into functions
+			form.submit(function(event) {
+				event.stopPropagation();
+				event.preventDefault();
+				if (input.hasClass('error')) {
+					return;
+				}
+
+				try {
+					var newName = input.val().trim();
+					input.tooltip('hide');
+					form.remove();
+
+					if (newName !== oldName) {
+						checkInput();
+						// mark as loading (temp element)
+						self.showFileBusyState(tr, true);
+						tr.attr('data-file', newName);
+						var basename = newName;
+						if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') {
+							basename = newName.substr(0, newName.lastIndexOf('.'));
+						}
+						td.find('a.name span.nametext').text(basename);
+						td.children('a.name').children(':not(.thumbnail-wrapper)').show();
+
+						var path = tr.attr('data-path') || self.getCurrentDirectory();
+						self.filesClient.move(OC.joinPaths(path, oldName), OC.joinPaths(path, newName))
+							.done(function() {
+								oldFileInfo.name = newName;
+								updateInList(oldFileInfo);
+							})
+							.fail(function(status) {
+								// TODO: 409 means current folder does not exist, redirect ?
+								if (status === 404) {
+									// source not found, so remove it from the list
+									OC.Notification.show(t('files', 'Could not rename "{fileName}", it does not exist any more',
+										{fileName: oldName}), {timeout: 7, type: 'error'}
+									);
+
+									self.remove(newName, {updateSummary: true});
+									return;
+								} else if (status === 412) {
+									// target exists
+									OC.Notification.show(
+										t('files', 'The name "{targetName}" is already used in the folder "{dir}". Please choose a different name.',
+										{
+											targetName: newName,
+											dir: self.getCurrentDirectory(),
+										}),
+										{
+											type: 'error'
+										}
+									);
+								} else {
+									// restore the item to its previous state
+									OC.Notification.show(t('files', 'Could not rename "{fileName}"',
+										{fileName: oldName}), {type: 'error'}
+									);
+								}
+								updateInList(oldFileInfo);
+							});
+					} else {
+						// add back the old file info when cancelled
+						self.files.splice(tr.index(), 1);
+						tr.remove();
+						tr = self.add(oldFileInfo, {updateSummary: false, silent: true});
+						self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
+					}
+				} catch (error) {
+					input.attr('title', error);
+					input.tooltip({placement: 'right', trigger: 'manual'});
+					input.tooltip('fixTitle');
+					input.tooltip('show');
+					input.addClass('error');
+				}
+				return false;
+			});
+			input.keyup(function(event) {
+				// verify filename on typing
+				try {
+					checkInput();
+					input.tooltip('hide');
+					input.removeClass('error');
+				} catch (error) {
+					input.attr('title', error);
+					input.tooltip({placement: 'right', trigger: 'manual'});
+					input.tooltip('fixTitle');
+					input.tooltip('show');
+					input.addClass('error');
+				}
+				if (event.keyCode === 27) {
+					restore();
+				}
+			});
+			input.click(function(event) {
+				event.stopPropagation();
+				event.preventDefault();
+			});
+			input.blur(function() {
+				if(input.hasClass('error')) {
+					restore();
+				} else {
+					form.trigger('submit');
+				}
+			});
+		},
+
+		/**
+		 * Create an empty file inside the current directory.
+		 *
+		 * @param {string} name name of the file
+		 *
+		 * @return {Promise} promise that will be resolved after the
+		 * file was created
+		 *
+		 * @since 8.2
+		 */
+		createFile: function(name) {
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			OCA.Files.Files.isFileNameValid(name);
+
+			if (this.lastAction) {
+				this.lastAction();
+			}
+
+			name = this.getUniqueName(name);
+			var targetPath = this.getCurrentDirectory() + '/' + name;
+
+			self.filesClient.putFileContents(
+					targetPath,
+					' ', // dont create empty files which fails on some storage backends
+					{
+						contentType: 'text/plain',
+						overwrite: true
+					}
+				)
+				.done(function() {
+					// TODO: error handling / conflicts
+					self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) {
+						deferred.resolve(status, data);
+					}, function() {
+						OC.Notification.show(t('files', 'Could not create file "{file}"',
+							{file: name}), {type: 'error'}
+						);
+					});
+				})
+				.fail(function(status) {
+					if (status === 412) {
+						OC.Notification.show(t('files', 'Could not create file "{file}" because it already exists',
+							{file: name}), {type: 'error'}
+						);
+					} else {
+						OC.Notification.show(t('files', 'Could not create file "{file}"',
+							{file: name}), {type: 'error'}
+						);
+					}
+					deferred.reject(status);
+				});
+
+			return promise;
+		},
+
+		/**
+		 * Create a directory inside the current directory.
+		 *
+		 * @param {string} name name of the directory
+		 *
+		 * @return {Promise} promise that will be resolved after the
+		 * directory was created
+		 *
+		 * @since 8.2
+		 */
+		createDirectory: function(name) {
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			OCA.Files.Files.isFileNameValid(name);
+
+			if (this.lastAction) {
+				this.lastAction();
+			}
+
+			name = this.getUniqueName(name);
+			var targetPath = this.getCurrentDirectory() + '/' + name;
+
+			this.filesClient.createDirectory(targetPath)
+				.done(function() {
+					self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) {
+						deferred.resolve(status, data);
+					}, function() {
+						OC.Notification.show(t('files', 'Could not create folder "{dir}"',
+							{dir: name}), {type: 'error'}
+						);
+					});
+				})
+				.fail(function(createStatus) {
+					// method not allowed, folder might exist already
+					if (createStatus === 405) {
+						// add it to the list, for completeness
+						self.addAndFetchFileInfo(targetPath, '', {scrollTo:true})
+							.done(function(status, data) {
+								OC.Notification.show(t('files', 'Could not create folder "{dir}" because it already exists',
+									{dir: name}), {type: 'error'}
+								);
+								// still consider a failure
+								deferred.reject(createStatus, data);
+							})
+							.fail(function() {
+								OC.Notification.show(t('files', 'Could not create folder "{dir}"',
+									{dir: name}), {type: 'error'}
+								);
+								deferred.reject(status);
+							});
+					} else {
+						OC.Notification.show(t('files', 'Could not create folder "{dir}"',
+							{dir: name}), {type: 'error'}
+						);
+						deferred.reject(createStatus);
+					}
+				});
+
+			return promise;
+		},
+
+		/**
+		 * Add file into the list by fetching its information from the server first.
+		 *
+		 * If the given directory does not match the current directory, nothing will
+		 * be fetched.
+		 *
+		 * @param {String} fileName file name
+		 * @param {String} [dir] optional directory, defaults to the current one
+		 * @param {Object} options same options as #add
+		 * @return {Promise} promise that resolves with the file info, or an
+		 * already resolved Promise if no info was fetched. The promise rejects
+		 * if the file was not found or an error occurred.
+		 *
+		 * @since 9.0
+		 */
+		addAndFetchFileInfo: function(fileName, dir, options) {
+			var self = this;
+			var deferred = $.Deferred();
+			if (_.isUndefined(dir)) {
+				dir = this.getCurrentDirectory();
+			} else {
+				dir = dir || '/';
+			}
+
+			var targetPath = OC.joinPaths(dir, fileName);
+
+			if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) {
+				// no need to fetch information
+				deferred.resolve();
+				return deferred.promise();
+			}
+
+			var addOptions = _.extend({
+				animate: true,
+				scrollTo: false
+			}, options || {});
+
+			this.filesClient.getFileInfo(targetPath, {
+					properties: this._getWebdavProperties()
+				})
+				.then(function(status, data) {
+					// remove first to avoid duplicates
+					self.remove(data.name);
+					self.add(data, addOptions);
+					deferred.resolve(status, data);
+				})
+				.fail(function(status) {
+					OC.Notification.show(t('files', 'Could not create file "{file}"',
+						{file: name}), {type: 'error'}
+					);
+					deferred.reject(status);
+				});
+
+			return deferred.promise();
+		},
+
+		/**
+		 * Returns whether the given file name exists in the list
+		 *
+		 * @param {string} file file name
+		 *
+		 * @return {bool} true if the file exists in the list, false otherwise
+		 */
+		inList:function(file) {
+			return this.findFile(file);
+		},
+
+		/**
+		 * Shows busy state on a given file row or multiple
+		 *
+		 * @param {string|Array.<string>} files file name or array of file names
+		 * @param {bool} [busy=true] busy state, true for busy, false to remove busy state
+		 *
+		 * @since 8.2
+		 */
+		showFileBusyState: function(files, state) {
+			var self = this;
+			if (!_.isArray(files) && !files.is) {
+				files = [files];
+			}
+
+			if (_.isUndefined(state)) {
+				state = true;
+			}
+
+			_.each(files, function(fileName) {
+				// jquery element already ?
+				var $tr;
+				if (_.isString(fileName)) {
+					$tr = self.findFileEl(fileName);
+				} else {
+					$tr = $(fileName);
+				}
+
+				var $thumbEl = $tr.find('.thumbnail');
+				$tr.toggleClass('busy', state);
+
+				if (state) {
+					$thumbEl.parent().addClass('icon-loading-small');
+				} else {
+					$thumbEl.parent().removeClass('icon-loading-small');
+				}
+			});
+		},
+
+		/**
+		 * Delete the given files from the given dir
+		 * @param files file names list (without path)
+		 * @param dir directory in which to delete the files, defaults to the current
+		 * directory
+		 */
+		do_delete:function(files, dir) {
+			var self = this;
+			if (files && files.substr) {
+				files=[files];
+			}
+			if (!files) {
+				// delete all files in directory
+				files = _.pluck(this.files, 'name');
+			}
+			// Finish any existing actions
+			if (this.lastAction) {
+				this.lastAction();
+			}
+
+			dir = dir || this.getCurrentDirectory();
+
+			var removeFunction = function(fileName) {
+				var $tr = self.findFileEl(fileName);
+				self.showFileBusyState($tr, true);
+				return self.filesClient.remove(dir + '/' + fileName)
+					.done(function() {
+						if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
+							self.remove(fileName);
+						}
+					})
+					.fail(function(status) {
+						if (status === 404) {
+							// the file already did not exist, remove it from the list
+							if (OC.joinPaths(self.getCurrentDirectory(), '/') === OC.joinPaths(dir, '/')) {
+								self.remove(fileName);
+							}
+						} else {
+							// only reset the spinner for that one file
+							OC.Notification.show(t('files', 'Error deleting file "{fileName}".',
+								{fileName: fileName}), {type: 'error'}
+							);
+						}
+					})
+					.always(function() {
+						self.showFileBusyState($tr, false);
+					});
+			};
+			return this.reportOperationProgress(files, removeFunction).then(function(){
+					self.updateStorageStatistics();
+					self.updateStorageQuotas();
+				});
+		},
+
+		/**
+		 * Creates the file summary section
+		 */
+		_createSummary: function() {
+			var $tr = $('<tr class="summary"></tr>');
+
+			if (this._allowSelection) {
+				// Dummy column for selection, as all rows must have the same
+				// number of columns.
+				$tr.append('<td></td>');
+			}
+
+			this.$el.find('tfoot').append($tr);
+
+			return new OCA.Files.FileSummary($tr, {config: this._filesConfig});
+		},
+		updateEmptyContent: function() {
+			var permissions = this.getDirectoryPermissions();
+			var isCreatable = (permissions & OC.PERMISSION_CREATE) !== 0;
+			this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
+			this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
+			this.$el.find('#emptycontent .uploadmessage').toggleClass('hidden', !isCreatable || !this.isEmpty);
+			this.$el.find('#filestable').toggleClass('hidden', this.isEmpty);
+			this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
+		},
+		/**
+		 * Shows the loading mask.
+		 *
+		 * @see OCA.Files.FileList#hideMask
+		 */
+		showMask: function() {
+			// in case one was shown before
+			var $mask = this.$el.find('.mask');
+			if ($mask.exists()) {
+				return;
+			}
+
+			this.$table.addClass('hidden');
+			this.$el.find('#emptycontent').addClass('hidden');
+
+			$mask = $('<div class="mask transparent icon-loading"></div>');
+
+			this.$el.append($mask);
+
+			$mask.removeClass('transparent');
+		},
+		/**
+		 * Hide the loading mask.
+		 * @see OCA.Files.FileList#showMask
+		 */
+		hideMask: function() {
+			this.$el.find('.mask').remove();
+			this.$table.removeClass('hidden');
+		},
+		scrollTo:function(file) {
+			if (!_.isArray(file)) {
+				file = [file];
+			}
+			if (file.length === 1) {
+				_.defer(function() {
+					this.showDetailsView(file[0]);
+				}.bind(this));
+			}
+			this.highlightFiles(file, function($tr) {
+				$tr.addClass('searchresult');
+				$tr.one('hover', function() {
+					$tr.removeClass('searchresult');
+				});
+			});
+		},
+		/**
+		 * @deprecated use setFilter(filter)
+		 */
+		filter:function(query) {
+			this.setFilter('');
+		},
+		/**
+		 * @deprecated use setFilter('')
+		 */
+		unfilter:function() {
+			this.setFilter('');
+		},
+		/**
+		 * hide files matching the given filter
+		 * @param filter
+		 */
+		setFilter:function(filter) {
+			var total = 0;
+			if (this._filter === filter) {
+				return;
+			}
+			this._filter = filter;
+			this.fileSummary.setFilter(filter, this.files);
+			total = this.fileSummary.getTotal();
+			if (!this.$el.find('.mask').exists()) {
+				this.hideIrrelevantUIWhenNoFilesMatch();
+			}
+
+			var visibleCount = 0;
+			filter = filter.toLowerCase();
+
+			function filterRows(tr) {
+				var $e = $(tr);
+				if ($e.data('file').toString().toLowerCase().indexOf(filter) === -1) {
+					$e.addClass('hidden');
+				} else {
+					visibleCount++;
+					$e.removeClass('hidden');
+				}
+			}
+
+			var $trs = this.$fileList.find('tr');
+			do {
+				_.each($trs, filterRows);
+				if (visibleCount < total) {
+					$trs = this._nextPage(false);
+				}
+			} while (visibleCount < total && $trs.length > 0);
+
+			this.$container.trigger('scroll');
+		},
+		hideIrrelevantUIWhenNoFilesMatch:function() {
+			if (this._filter && this.fileSummary.summary.totalDirs + this.fileSummary.summary.totalFiles === 0) {
+				this.$el.find('#filestable thead th').addClass('hidden');
+				this.$el.find('#emptycontent').addClass('hidden');
+				$('#searchresults').addClass('filter-empty');
+				$('#searchresults .emptycontent').addClass('emptycontent-search');
+				if ( $('#searchresults').length === 0 || $('#searchresults').hasClass('hidden') ) {
+					var error = t('files', 'No search results in other folders for {tag}{filter}{endtag}', {filter:this._filter});
+					this.$el.find('.nofilterresults').removeClass('hidden').
+						find('p').html(error.replace('{tag}', '<strong>').replace('{endtag}', '</strong>'));
+				}
+			} else {
+				$('#searchresults').removeClass('filter-empty');
+				$('#searchresults .emptycontent').removeClass('emptycontent-search');
+				this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
+				if (!this.$el.find('.mask').exists()) {
+					this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
+				}
+				this.$el.find('.nofilterresults').addClass('hidden');
+			}
+		},
+		/**
+		 * get the current filter
+		 * @param filter
+		 */
+		getFilter:function(filter) {
+			return this._filter;
+		},
+		/**
+		 * update the search object to use this filelist when filtering
+		 */
+		updateSearch:function() {
+			if (OCA.Search.files) {
+				OCA.Search.files.setFileList(this);
+			}
+			if (OC.Search) {
+				OC.Search.clear();
+			}
+		},
+		/**
+		 * Update UI based on the current selection
+		 */
+		updateSelectionSummary: function() {
+			var summary = this._selectionSummary.summary;
+			var selection;
+
+			var showHidden = !!this._filesConfig.get('showhidden');
+			if (summary.totalFiles === 0 && summary.totalDirs === 0) {
+				this.$el.find('#headerName a.name>span:first').text(t('files','Name'));
+				this.$el.find('#headerSize a>span:first').text(t('files','Size'));
+				this.$el.find('#modified a>span:first').text(t('files','Modified'));
+				this.$el.find('table').removeClass('multiselect');
+				this.$el.find('.selectedActions').addClass('hidden');
+			}
+			else {
+				this.$el.find('.selectedActions').removeClass('hidden');
+				this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize));
+
+				var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs);
+				var fileInfo = n('files', '%n file', '%n files', summary.totalFiles);
+
+				if (summary.totalDirs > 0 && summary.totalFiles > 0) {
+					var selectionVars = {
+						dirs: directoryInfo,
+						files: fileInfo
+					};
+					selection = t('files', '{dirs} and {files}', selectionVars);
+				} else if (summary.totalDirs > 0) {
+					selection = directoryInfo;
+				} else {
+					selection = fileInfo;
+				}
+
+				if (!showHidden && summary.totalHidden > 0) {
+					var hiddenInfo = n('files', 'including %n hidden', 'including %n hidden', summary.totalHidden);
+					selection += ' (' + hiddenInfo + ')';
+				}
+
+				this.$el.find('#headerName a.name>span:first').text(selection);
+				this.$el.find('#modified a>span:first').text('');
+				this.$el.find('table').addClass('multiselect');
+
+				if (this.fileMultiSelectMenu) {
+					this.fileMultiSelectMenu.toggleItemVisibility('download', this.isSelectedDownloadable());
+					this.fileMultiSelectMenu.toggleItemVisibility('delete', this.isSelectedDeletable());
+					this.fileMultiSelectMenu.toggleItemVisibility('copyMove', this.isSelectedCopiable());
+					if (this.isSelectedCopiable()) {
+						if (this.isSelectedMovable()) {
+							this.fileMultiSelectMenu.updateItemText('copyMove', t('files', 'Move or copy'));
+						} else {
+							this.fileMultiSelectMenu.updateItemText('copyMove', t('files', 'Copy'));
+						}
+					} else {
+						this.fileMultiSelectMenu.toggleItemVisibility('copyMove', false);
+					}
+				}
+			}
+		},
+
+		/**
+		 * Check whether all selected files are copiable
+		 */
+		isSelectedCopiable: function() {
+			return _.reduce(this.getSelectedFiles(), function(copiable, file) {
+				var requiredPermission = $('#isPublic').val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ;
+				return copiable && (file.permissions & requiredPermission);
+			}, true);
+		},
+
+		/**
+		 * Check whether all selected files are movable
+		 */
+		isSelectedMovable: function() {
+			return _.reduce(this.getSelectedFiles(), function(movable, file) {
+				return movable && (file.permissions & OC.PERMISSION_UPDATE);
+			}, true);
+		},
+
+		/**
+		 * Check whether all selected files are downloadable
+		 */
+		isSelectedDownloadable: function() {
+			return _.reduce(this.getSelectedFiles(), function(downloadable, file) {
+				return downloadable && (file.permissions & OC.PERMISSION_READ);
+			}, true);
+		},
+
+		/**
+		 * Check whether all selected files are deletable
+		 */
+		isSelectedDeletable: function() {
+			return _.reduce(this.getSelectedFiles(), function(deletable, file) {
+				return deletable && (file.permissions & OC.PERMISSION_DELETE);
+			}, true);
+		},
+
+		/**
+		 * Are all files selected?
+		 * 
+		 * @returns {Boolean} all files are selected
+		 */
+		isAllSelected: function() {
+			var checkbox = this.$el.find('.select-all')
+			var checked = checkbox.prop('checked')
+			var indeterminate = checkbox.prop('indeterminate')
+			return checked && !indeterminate;
+		},
+
+		/**
+		 * Returns the file info of the selected files
+		 *
+		 * @return array of file names
+		 */
+		getSelectedFiles: function() {
+			return _.values(this._selectedFiles);
+		},
+
+		getUniqueName: function(name) {
+			if (this.findFileEl(name).exists()) {
+				var numMatch;
+				var parts=name.split('.');
+				var extension = "";
+				if (parts.length > 1) {
+					extension=parts.pop();
+				}
+				var base=parts.join('.');
+				numMatch=base.match(/\((\d+)\)/);
+				var num=2;
+				if (numMatch && numMatch.length>0) {
+					num=parseInt(numMatch[numMatch.length-1], 10)+1;
+					base=base.split('(');
+					base.pop();
+					base=$.trim(base.join('('));
+				}
+				name=base+' ('+num+')';
+				if (extension) {
+					name = name+'.'+extension;
+				}
+				// FIXME: ugly recursion
+				return this.getUniqueName(name);
+			}
+			return name;
+		},
+
+		/**
+		 * Shows a "permission denied" notification
+		 */
+		_showPermissionDeniedNotification: function() {
+			var message = t('files', 'You don’t have permission to upload or create files here');
+			OC.Notification.show(message, {type: 'error'});
+		},
+
+		/**
+		 * Setup file upload events related to the file-upload plugin
+		 *
+		 * @param {OC.Uploader} uploader
+		 */
+		setupUploadEvents: function(uploader) {
+			var self = this;
+
+			self._uploads = {};
+
+			// detect the progress bar resize
+			uploader.on('resized', this._onResize);
+
+			uploader.on('drop', function(e, data) {
+				self._uploader.log('filelist handle fileuploaddrop', e, data);
+
+				if (self.$el.hasClass('hidden')) {
+					// do not upload to invisible lists
+					e.preventDefault();
+					return false;
+				}
+
+				var dropTarget = $(e.delegatedEvent.target);
+
+				// check if dropped inside this container and not another one
+				if (dropTarget.length
+					&& !self.$el.is(dropTarget) // dropped on list directly
+					&& !self.$el.has(dropTarget).length // dropped inside list
+					&& !dropTarget.is(self.$container) // dropped on main container
+					&& !self.$el.parent().is(dropTarget) // drop on the parent container (#app-content) since the main container might not have the full height
+					) {
+					e.preventDefault();
+					return false;
+				}
+
+				// find the closest tr or crumb to use as target
+				dropTarget = dropTarget.closest('tr, .crumb');
+
+				// if dropping on tr or crumb, drag&drop upload to folder
+				if (dropTarget && (dropTarget.data('type') === 'dir' ||
+					dropTarget.hasClass('crumb'))) {
+
+					// remember as context
+					data.context = dropTarget;
+
+					// if permissions are specified, only allow if create permission is there
+					var permissions = dropTarget.data('permissions');
+					if (!_.isUndefined(permissions) && (permissions & OC.PERMISSION_CREATE) === 0) {
+						self._showPermissionDeniedNotification();
+						return false;
+					}
+					var dir = dropTarget.data('file');
+					// if from file list, need to prepend parent dir
+					if (dir) {
+						var parentDir = self.getCurrentDirectory();
+						if (parentDir[parentDir.length - 1] !== '/') {
+							parentDir += '/';
+						}
+						dir = parentDir + dir;
+					}
+					else{
+						// read full path from crumb
+						dir = dropTarget.data('dir') || '/';
+					}
+
+					// add target dir
+					data.targetDir = dir;
+				} else {
+					// cancel uploads to current dir if no permission
+					var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0;
+					if (!isCreatable) {
+						self._showPermissionDeniedNotification();
+						e.stopPropagation();
+						return false;
+					}
+
+					// we are dropping somewhere inside the file list, which will
+					// upload the file to the current directory
+					data.targetDir = self.getCurrentDirectory();
+				}
+			});
+			uploader.on('add', function(e, data) {
+				self._uploader.log('filelist handle fileuploadadd', e, data);
+
+				// add ui visualization to existing folder
+				if (data.context && data.context.data('type') === 'dir') {
+					// add to existing folder
+
+					// update upload counter ui
+					var uploadText = data.context.find('.uploadtext');
+					var currentUploads = parseInt(uploadText.attr('currentUploads'), 10);
+					currentUploads += 1;
+					uploadText.attr('currentUploads', currentUploads);
+
+					var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads);
+					if (currentUploads === 1) {
+						self.showFileBusyState(uploadText.closest('tr'), true);
+						uploadText.text(translatedText);
+						uploadText.show();
+					} else {
+						uploadText.text(translatedText);
+					}
+				}
+
+				if (!data.targetDir) {
+					data.targetDir = self.getCurrentDirectory();
+				}
+
+			});
+			/*
+			 * when file upload done successfully add row to filelist
+			 * update counter when uploading to sub folder
+			 */
+			uploader.on('done', function(e, upload) {
+				var data = upload.data;
+				self._uploader.log('filelist handle fileuploaddone', e, data);
+
+				var status = data.jqXHR.status;
+				if (status < 200 || status >= 300) {
+					// error was handled in OC.Uploads already
+					return;
+				}
+
+				var fileName = upload.getFileName();
+				var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath());
+				if (!self._uploads) {
+					self._uploads = {};
+				}
+				if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) {
+					self._uploads[fileName] = fetchInfoPromise;
+				}
+
+				var uploadText = self.$fileList.find('tr .uploadtext');
+				self.showFileBusyState(uploadText.closest('tr'), false);
+				uploadText.fadeOut();
+				uploadText.attr('currentUploads', 0);
+
+				self.updateStorageQuotas();
+			});
+			uploader.on('createdfolder', function(fullPath) {
+				self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath));
+			});
+			uploader.on('stop', function() {
+				self._uploader.log('filelist handle fileuploadstop');
+
+				// prepare list of uploaded file names in the current directory
+				// and discard the other ones
+				var promises = _.values(self._uploads);
+				var fileNames = _.keys(self._uploads);
+				self._uploads = [];
+
+				// as soon as all info is fetched
+				$.when.apply($, promises).then(function() {
+					// highlight uploaded files
+					self.highlightFiles(fileNames);
+					self.updateStorageStatistics();
+				});
+
+				var uploadText = self.$fileList.find('tr .uploadtext');
+				self.showFileBusyState(uploadText.closest('tr'), false);
+				uploadText.fadeOut();
+				uploadText.attr('currentUploads', 0);
+			});
+			uploader.on('fail', function(e, data) {
+				self._uploader.log('filelist handle fileuploadfail', e, data);
+				self._uploads = [];
+
+				//if user pressed cancel hide upload chrome
+				//cleanup uploading to a dir
+				var uploadText = self.$fileList.find('tr .uploadtext');
+				self.showFileBusyState(uploadText.closest('tr'), false);
+				uploadText.fadeOut();
+				uploadText.attr('currentUploads', 0);
+				self.updateStorageStatistics();
+			});
+
+		},
+
+		/**
+		 * Scroll to the last file of the given list
+		 * Highlight the list of files
+		 * @param files array of filenames,
+		 * @param {Function} [highlightFunction] optional function
+		 * to be called after the scrolling is finished
+		 */
+		highlightFiles: function(files, highlightFunction) {
+			// Detection of the uploaded element
+			var filename = files[files.length - 1];
+			var $fileRow = this.findFileEl(filename);
+
+			while(!$fileRow.exists() && this._nextPage(false) !== false) { // Checking element existence
+				$fileRow = this.findFileEl(filename);
+			}
+
+			if (!$fileRow.exists()) { // Element not present in the file list
+				return;
+			}
+
+			var currentOffset = this.$container.scrollTop();
+			var additionalOffset = this.$el.find("#controls").height()+this.$el.find("#controls").offset().top;
+
+			// Animation
+			var _this = this;
+			var $scrollContainer = this.$container;
+			if ($scrollContainer[0] === window) {
+				// need to use "html" to animate scrolling
+				// when the scroll container is the window
+				$scrollContainer = $('html');
+			}
+			$scrollContainer.animate({
+				// Scrolling to the top of the new element
+				scrollTop: currentOffset + $fileRow.offset().top - $fileRow.height() * 2 - additionalOffset
+			}, {
+				duration: 500,
+				complete: function() {
+					// Highlighting function
+					var highlightRow = highlightFunction;
+
+					if (!highlightRow) {
+						highlightRow = function($fileRow) {
+							$fileRow.addClass("highlightUploaded");
+							setTimeout(function() {
+								$fileRow.removeClass("highlightUploaded");
+							}, 2500);
+						};
+					}
+
+					// Loop over uploaded files
+					for(var i=0; i<files.length; i++) {
+						var $fileRow = _this.findFileEl(files[i]);
+
+						if($fileRow.length !== 0) { // Checking element existence
+							highlightRow($fileRow);
+						}
+					}
+
+				}
+			});
+		},
+
+		_renderNewButton: function() {
+			// if an upload button (legacy) already exists or no actions container exist, skip
+			var $actionsContainer = this.$el.find('#controls .actions');
+			if (!$actionsContainer.length || this.$el.find('.button.upload').length) {
+				return;
+			}
+			var $newButton = $(OCA.Files.Templates['template_addbutton']({
+				addText: t('files', 'New'),
+				iconClass: 'icon-add'
+			}));
+
+			$actionsContainer.prepend($newButton);
+			$newButton.tooltip({'placement': 'bottom'});
+
+			$newButton.click(_.bind(this._onClickNewButton, this));
+			this._newButton = $newButton;
+		},
+
+		_onClickNewButton: function(event) {
+			var $target = $(event.target);
+			if (!$target.hasClass('.button')) {
+				$target = $target.closest('.button');
+			}
+			this._newButton.tooltip('hide');
+			event.preventDefault();
+			if ($target.hasClass('disabled')) {
+				return false;
+			}
+			if (!this._newFileMenu) {
+				this._newFileMenu = new OCA.Files.NewFileMenu({
+					fileList: this
+				});
+				$('.actions').append(this._newFileMenu.$el);
+			}
+			this._newFileMenu.showAt($target);
+
+			return false;
+		},
+
+		/**
+		 * Register a tab view to be added to all views
+		 */
+		registerTabView: function(tabView) {
+			if (this._detailsView) {
+				this._detailsView.addTabView(tabView);
+			}
+		},
+
+		/**
+		 * Register a detail view to be added to all views
+		 */
+		registerDetailView: function(detailView) {
+			if (this._detailsView) {
+				this._detailsView.addDetailView(detailView);
+			}
+		},
+
+		/**
+		 * Register a view to be added to the breadcrumb view
+		 */
+		registerBreadCrumbDetailView: function(detailView) {
+			if (this.breadcrumb) {
+				this.breadcrumb.addDetailView(detailView);
+			}
+		},
+
+		/**
+		 * Returns the registered detail views.
+		 *
+		 * @return null|Array<OCA.Files.DetailFileInfoView> an array with the
+		 *         registered DetailFileInfoViews, or null if the details view
+		 *         is not enabled.
+		 */
+		getRegisteredDetailViews: function() {
+			if (this._detailsView) {
+				return this._detailsView.getDetailViews();
+			}
+
+			return null;
+		}
+	};
+
+	FileList.MultiSelectMenuActions = {
+		ToggleSelectionModeAction: function(fileList) {
+			return {
+				name: 'toggleSelectionMode',
+				displayName: function(context) {
+					return t('files', 'Select file range');
+				},
+				iconClass: 'icon-fullscreen',
+				action: function() {
+					fileList._onClickToggleSelectionMode();
+				},
+			};
+		},
+	},
+
+	/**
+	 * Sort comparators.
+	 * @namespace OCA.Files.FileList.Comparators
+	 * @private
+	 */
+	FileList.Comparators = {
+		/**
+		 * Compares two file infos by name, making directories appear
+		 * first.
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo1 file info
+		 * @param {OC.Files.FileInfo} fileInfo2 file info
+		 * @return {int} -1 if the first file must appear before the second one,
+		 * 0 if they are identify, 1 otherwise.
+		 */
+		name: function(fileInfo1, fileInfo2) {
+			if (fileInfo1.type === 'dir' && fileInfo2.type !== 'dir') {
+				return -1;
+			}
+			if (fileInfo1.type !== 'dir' && fileInfo2.type === 'dir') {
+				return 1;
+			}
+			return OC.Util.naturalSortCompare(fileInfo1.name, fileInfo2.name);
+		},
+		/**
+		 * Compares two file infos by size.
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo1 file info
+		 * @param {OC.Files.FileInfo} fileInfo2 file info
+		 * @return {int} -1 if the first file must appear before the second one,
+		 * 0 if they are identify, 1 otherwise.
+		 */
+		size: function(fileInfo1, fileInfo2) {
+			return fileInfo1.size - fileInfo2.size;
+		},
+		/**
+		 * Compares two file infos by timestamp.
+		 *
+		 * @param {OC.Files.FileInfo} fileInfo1 file info
+		 * @param {OC.Files.FileInfo} fileInfo2 file info
+		 * @return {int} -1 if the first file must appear before the second one,
+		 * 0 if they are identify, 1 otherwise.
+		 */
+		mtime: function(fileInfo1, fileInfo2) {
+			return fileInfo1.mtime - fileInfo2.mtime;
+		}
+	};
+
+	/**
+	 * File info attributes.
+	 *
+	 * @typedef {Object} OC.Files.FileInfo
+	 *
+	 * @lends OC.Files.FileInfo
+	 *
+	 * @deprecated use OC.Files.FileInfo instead
+	 *
+	 */
+	OCA.Files.FileInfo = OC.Files.FileInfo;
+
+	OCA.Files.FileList = FileList;
+})();
+
+$(document).ready(function() {
+	// FIXME: unused ?
+	OCA.Files.FileList.useUndo = (window.onbeforeunload)?true:false;
+	$(window).on('beforeunload', function () {
+		if (OCA.Files.FileList.lastAction) {
+			OCA.Files.FileList.lastAction();
+		}
+	});
+	$(window).on('unload', function () {
+		$(window).trigger('beforeunload');
+	});
+
+});
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+(function() {
+
+	/**
+	 * Construct a new FileActions instance
+	 * @constructs Files
+	 */
+	var Files = function() {
+		this.initialize();
+	};
+	/**
+	 * @memberof OCA.Search
+	 */
+	Files.prototype = {
+
+		fileList: null,
+
+		/**
+		 * Initialize the file search
+		 */
+		initialize: function() {
+
+			var self = this;
+
+			this.fileAppLoaded = function() {
+				return !!OCA.Files && !!OCA.Files.App;
+			};
+			function inFileList($row, result) {
+				if (! self.fileAppLoaded()) {
+					return false;
+				}
+				var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,'');
+				var resultDir = OC.dirname(result.path);
+				return dir === resultDir && self.fileList.inList(result.name);
+			}
+			function updateLegacyMimetype(result) {
+				// backward compatibility:
+				if (!result.mime && result.mime_type) {
+					result.mime = result.mime_type;
+				}
+			}
+			function hideNoFilterResults() {
+				var $nofilterresults = $('.nofilterresults');
+				if ( ! $nofilterresults.hasClass('hidden') ) {
+					$nofilterresults.addClass('hidden');
+				}
+			}
+
+			this.renderFolderResult = function($row, result) {
+				if (inFileList($row, result)) {
+					return null;
+				}
+				hideNoFilterResults();
+				/*render folder icon, show path beneath filename,
+				 show size and last modified date on the right */
+				this.updateLegacyMimetype(result);
+
+				var $pathDiv = $('<div class="path"></div>').text(result.path.substr(1, result.path.lastIndexOf("/")));
+				$row.find('td.info div.name').after($pathDiv).text(result.name);
+
+				$row.find('td.result a').attr('href', result.link);
+				$row.find('td.icon').css('background-image', 'url(' +  OC.MimeType.getIconUrl(result.mime) + ')');
+				return $row;
+			};
+
+			this.renderFileResult = function($row, result) {
+				if (inFileList($row, result)) {
+					return null;
+				}
+				hideNoFilterResults();
+				/*render preview icon, show path beneath filename,
+				 show size and last modified date on the right */
+				this.updateLegacyMimetype(result);
+
+				var $pathDiv = $('<div class="path"></div>').text(result.path.substr(1, result.path.lastIndexOf("/")));
+				$row.find('td.info div.name').after($pathDiv).text(result.name);
+
+				$row.find('td.result a').attr('href', result.link);
+
+				if (self.fileAppLoaded()) {
+					self.fileList.lazyLoadPreview({
+						path: result.path,
+						mime: result.mime,
+						callback: function (url) {
+							$row.find('td.icon').css('background-image', 'url(' + url + ')');
+						}
+					});
+				} else {
+					// FIXME how to get mime icon if not in files app
+					var mimeicon = result.mime.replace('/', '-');
+					$row.find('td.icon').css('background-image', 'url(' + OC.MimeType.getIconUrl(result.mime) + ')');
+					var dir = OC.dirname(result.path);
+					if (dir === '') {
+						dir = '/';
+					}
+					$row.find('td.info a').attr('href',
+						OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.name})
+					);
+				}
+				return $row;
+			};
+
+
+			this.handleFolderClick = function($row, result, event) {
+				// open folder
+				if (self.fileAppLoaded() && self.fileList.id === 'files') {
+					self.fileList.changeDirectory(result.path);
+					return false;
+				} else {
+					return true;
+				}
+			};
+
+			this.handleFileClick = function($row, result, event) {
+				if (self.fileAppLoaded() && self.fileList.id === 'files') {
+					self.fileList.changeDirectory(OC.dirname(result.path));
+					self.fileList.scrollTo(result.name);
+					return false;
+				} else {
+					return true;
+				}
+			};
+
+			this.updateLegacyMimetype = function (result) {
+				// backward compatibility:
+				if (!result.mime && result.mime_type) {
+					result.mime = result.mime_type;
+				}
+			};
+			this.setFileList = function (fileList) {
+				this.fileList = fileList;
+			};
+
+			OC.Plugins.register('OCA.Search.Core', this);
+		},
+		attach: function(search) {
+			var self = this;
+			search.setFilter('files', function (query) {
+				if (self.fileAppLoaded()) {
+					self.fileList.setFilter(query);
+					if (query.length > 2) {
+						//search is not started until 500msec have passed
+						window.setTimeout(function() {
+							$('.nofilterresults').addClass('hidden');
+						}, 500);
+					}
+				}
+			});
+
+			search.setRenderer('folder', this.renderFolderResult.bind(this));
+			search.setRenderer('file',   this.renderFileResult.bind(this));
+			search.setRenderer('image',   this.renderFileResult.bind(this));
+			search.setRenderer('audio',   this.renderFileResult.bind(this));
+
+			search.setHandler('folder',  this.handleFolderClick.bind(this));
+			search.setHandler(['file', 'audio', 'image'], this.handleFileClick.bind(this));
+
+			if (self.fileAppLoaded()) {
+				// hide results when switching directory outside of search results
+				$('#app-content').delegate('>div', 'changeDirectory', function() {
+					search.clear();
+				});
+			}
+		}
+	};
+	OCA.Search.Files = Files;
+	OCA.Search.files = new Files();
+})();
+
+
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+// HACK: this piece needs to be loaded AFTER the files app (for unit tests)
+$(document).ready(function() {
+	(function(OCA) {
+		/**
+		 * @class OCA.Files.FavoritesFileList
+		 * @augments OCA.Files.FavoritesFileList
+		 *
+		 * @classdesc Favorites file list.
+		 * Displays the list of files marked as favorites
+		 *
+		 * @param $el container element with existing markup for the #controls
+		 * and a table
+		 * @param [options] map of options, see other parameters
+		 */
+		var FavoritesFileList = function($el, options) {
+			this.initialize($el, options);
+		};
+		FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
+			/** @lends OCA.Files.FavoritesFileList.prototype */ {
+			id: 'favorites',
+			appName: t('files','Favorites'),
+
+			_clientSideSort: true,
+			_allowSelection: false,
+
+			/**
+			 * @private
+			 */
+			initialize: function($el, options) {
+				OCA.Files.FileList.prototype.initialize.apply(this, arguments);
+				if (this.initialized) {
+					return;
+				}
+				OC.Plugins.attach('OCA.Files.FavoritesFileList', this);
+			},
+
+			updateEmptyContent: function() {
+				var dir = this.getCurrentDirectory();
+				if (dir === '/') {
+					// root has special permissions
+					this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
+					this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
+				}
+				else {
+					OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
+				}
+			},
+
+			getDirectoryPermissions: function() {
+				return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
+			},
+
+			updateStorageStatistics: function() {
+				// no op because it doesn't have
+				// storage info like free space / used space
+			},
+
+			reload: function() {
+				this.showMask();
+				if (this._reloadCall) {
+					this._reloadCall.abort();
+				}
+
+				// there is only root
+				this._setCurrentDir('/', false);
+
+				this._reloadCall = this.filesClient.getFilteredFiles(
+					{
+						favorite: true
+					},
+					{
+						properties: this._getWebdavProperties()
+					}
+				);
+				var callBack = this.reloadCallback.bind(this);
+				return this._reloadCall.then(callBack, callBack);
+			},
+
+			reloadCallback: function(status, result) {
+				if (result) {
+					// prepend empty dir info because original handler
+					result.unshift({});
+				}
+
+				return OCA.Files.FileList.prototype.reloadCallback.call(this, status, result);
+			},
+		});
+
+		OCA.Files.FavoritesFileList = FavoritesFileList;
+	})(OCA);
+});
+
+
+
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+// HACK: this piece needs to be loaded AFTER the files app (for unit tests)
+$(document).ready(function () {
+	(function (OCA) {
+		/**
+		 * @class OCA.Files.RecentFileList
+		 * @augments OCA.Files.RecentFileList
+		 *
+		 * @classdesc Recent file list.
+		 * Displays the list of recently modified files
+		 *
+		 * @param $el container element with existing markup for the #controls
+		 * and a table
+		 * @param [options] map of options, see other parameters
+		 */
+		var RecentFileList = function ($el, options) {
+			options.sorting = {
+				mode: 'mtime',
+				direction: 'desc'
+			};
+			this.initialize($el, options);
+			this._allowSorting = false;
+		};
+		RecentFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
+			/** @lends OCA.Files.RecentFileList.prototype */ {
+				id: 'recent',
+				appName: t('files', 'Recent'),
+
+				_clientSideSort: true,
+				_allowSelection: false,
+
+				/**
+				 * @private
+				 */
+				initialize: function () {
+					OCA.Files.FileList.prototype.initialize.apply(this, arguments);
+					if (this.initialized) {
+						return;
+					}
+					OC.Plugins.attach('OCA.Files.RecentFileList', this);
+				},
+
+				updateEmptyContent: function () {
+					var dir = this.getCurrentDirectory();
+					if (dir === '/') {
+						// root has special permissions
+						this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
+						this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
+					}
+					else {
+						OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
+					}
+				},
+
+				getDirectoryPermissions: function () {
+					return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
+				},
+
+				updateStorageStatistics: function () {
+					// no op because it doesn't have
+					// storage info like free space / used space
+				},
+
+				reload: function () {
+					this.showMask();
+					if (this._reloadCall) {
+						this._reloadCall.abort();
+					}
+
+					// there is only root
+					this._setCurrentDir('/', false);
+
+					this._reloadCall = $.ajax({
+						url: OC.generateUrl('/apps/files/api/v1/recent'),
+						type: 'GET',
+						dataType: 'json'
+					});
+					var callBack = this.reloadCallback.bind(this);
+					return this._reloadCall.then(callBack, callBack);
+				},
+
+				reloadCallback: function (result) {
+					delete this._reloadCall;
+					this.hideMask();
+
+					if (result.files) {
+						this.setFiles(result.files.sort(this._sortComparator));
+						return true;
+					}
+					return false;
+				}
+			});
+
+		OCA.Files.RecentFileList = RecentFileList;
+	})(OCA);
+});
+
+
+
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global Handlebars */
+
+(function (OCA) {
+
+	_.extend(OC.Files.Client, {
+		PROPERTY_TAGS: '{' + OC.Files.Client.NS_OWNCLOUD + '}tags',
+		PROPERTY_FAVORITE: '{' + OC.Files.Client.NS_OWNCLOUD + '}favorite'
+	});
+
+	/**
+	 * Returns the icon class for the matching state
+	 *
+	 * @param {boolean} state true if starred, false otherwise
+	 * @return {string} icon class for star image
+	 */
+	function getStarIconClass (state) {
+		return state ? 'icon-starred' : 'icon-star';
+	}
+
+	/**
+	 * Render the star icon with the given state
+	 *
+	 * @param {boolean} state true if starred, false otherwise
+	 * @return {Object} jQuery object
+	 */
+	function renderStar (state) {
+		return OCA.Files.Templates['favorite_mark']({
+			isFavorite: state,
+			altText: state ? t('files', 'Favorited') : t('files', 'Not favorited'),
+			iconClass: getStarIconClass(state)
+		});
+	}
+
+	/**
+	 * Toggle star icon on favorite mark element
+	 *
+	 * @param {Object} $favoriteMarkEl favorite mark element
+	 * @param {boolean} state true if starred, false otherwise
+	 */
+	function toggleStar ($favoriteMarkEl, state) {
+		$favoriteMarkEl.removeClass('icon-star icon-starred').addClass(getStarIconClass(state));
+		$favoriteMarkEl.toggleClass('permanent', state);
+	}
+
+	/**
+	 * Remove Item from Quickaccesslist
+	 *
+	 * @param {String} appfolder folder to be removed
+	 */
+	function removeFavoriteFromList (appfolder) {
+		var quickAccessList = 'sublist-favorites';
+		var listULElements = document.getElementById(quickAccessList);
+		if (!listULElements) {
+			return;
+		}
+
+		var apppath=appfolder;
+		if(appfolder.startsWith("//")){
+			apppath=appfolder.substring(1, appfolder.length);
+		}
+
+		$(listULElements).find('[data-dir="' + apppath + '"]').remove();
+
+		if (listULElements.childElementCount === 0) {
+			var collapsibleButton = $(listULElements).parent().find('button.collapse');
+			collapsibleButton.hide();
+			$("#button-collapse-parent-favorites").removeClass('collapsible');
+		}
+	}
+
+	/**
+	 * Add Item to Quickaccesslist
+	 *
+	 * @param {String} appfolder folder to be added
+	 */
+	function addFavoriteToList (appfolder) {
+		var quickAccessList = 'sublist-favorites';
+		var listULElements = document.getElementById(quickAccessList);
+		if (!listULElements) {
+			return;
+		}
+		var listLIElements = listULElements.getElementsByTagName('li');
+
+		var appName = appfolder.substring(appfolder.lastIndexOf("/") + 1, appfolder.length);
+		var apppath = appfolder;
+
+		if(appfolder.startsWith("//")){
+			apppath = appfolder.substring(1, appfolder.length);
+		}
+		var url = OC.generateUrl('/apps/files/?dir=' + apppath + '&view=files');
+		
+
+		var innerTagA = document.createElement('A');
+		innerTagA.setAttribute("href", url);
+		innerTagA.setAttribute("class", "nav-icon-files svg");
+		innerTagA.innerHTML = _.escape(appName);
+
+		var length = listLIElements.length + 1;
+		var innerTagLI = document.createElement('li');
+		innerTagLI.setAttribute("data-id", apppath.replace('/', '-'));
+		innerTagLI.setAttribute("data-dir", apppath);
+		innerTagLI.setAttribute("data-view", 'files');
+		innerTagLI.setAttribute("class", "nav-" + appName);
+		innerTagLI.setAttribute("folderpos", length.toString());
+		innerTagLI.appendChild(innerTagA);
+
+		$.get(OC.generateUrl("/apps/files/api/v1/quickaccess/get/NodeType"),{folderpath: apppath}, function (data, status) {
+				if (data === "dir") {
+					if (listULElements.childElementCount <= 0) {
+						listULElements.appendChild(innerTagLI);
+						var collapsibleButton = $(listULElements).parent().find('button.collapse');
+						collapsibleButton.show();
+						$(listULElements).parent().addClass('collapsible');
+					} else {
+						listLIElements[listLIElements.length - 1].after(innerTagLI);
+					}
+				}
+			}
+		);
+	}
+
+	OCA.Files = OCA.Files || {};
+
+	/**
+	 * Extends the file actions and file list to include a favorite mark icon
+	 * and a favorite action in the file actions menu; it also adds "data-tags"
+	 * and "data-favorite" attributes to file elements.
+	 *
+	 * @namespace OCA.Files.TagsPlugin
+	 */
+	OCA.Files.TagsPlugin = {
+		name: 'Tags',
+
+		allowedLists: [
+			'files',
+			'favorites',
+			'systemtags',
+			'shares.self',
+			'shares.others',
+			'shares.link'
+		],
+
+		_extendFileActions: function (fileActions) {
+			var self = this;
+
+			fileActions.registerAction({
+				name: 'Favorite',
+				displayName: function (context) {
+					var $file = context.$file;
+					var isFavorite = $file.data('favorite') === true;
+
+					if (isFavorite) {
+						return t('files', 'Remove from favorites');
+					}
+
+					// As it is currently not possible to provide a context for
+					// the i18n strings "Add to favorites" was used instead of
+					// "Favorite" to remove the ambiguity between verb and noun
+					// when it is translated.
+					return t('files', 'Add to favorites');
+				},
+				mime: 'all',
+				order: -100,
+				permissions: OC.PERMISSION_NONE,
+				iconClass: function (fileName, context) {
+					var $file = context.$file;
+					var isFavorite = $file.data('favorite') === true;
+
+					if (isFavorite) {
+						return 'icon-star-dark';
+					}
+
+					return 'icon-starred';
+				},
+				actionHandler: function (fileName, context) {
+					var $favoriteMarkEl = context.$file.find('.favorite-mark');
+					var $file = context.$file;
+					var fileInfo = context.fileList.files[$file.index()];
+					var dir = context.dir || context.fileList.getCurrentDirectory();
+					var tags = $file.attr('data-tags');
+
+					if (_.isUndefined(tags)) {
+						tags = '';
+					}
+					tags = tags.split('|');
+					tags = _.without(tags, '');
+					var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0;
+					if (isFavorite) {
+						// remove tag from list
+						tags = _.without(tags, OC.TAG_FAVORITE);
+						removeFavoriteFromList(dir + '/' + fileName);
+					} else {
+						tags.push(OC.TAG_FAVORITE);
+						addFavoriteToList(dir + '/' + fileName);
+					}
+
+					// pre-toggle the star
+					toggleStar($favoriteMarkEl, !isFavorite);
+
+					context.fileInfoModel.trigger('busy', context.fileInfoModel, true);
+
+					self.applyFileTags(
+						dir + '/' + fileName,
+						tags,
+						$favoriteMarkEl,
+						isFavorite
+					).then(function (result) {
+						context.fileInfoModel.trigger('busy', context.fileInfoModel, false);
+						// response from server should contain updated tags
+						var newTags = result.tags;
+						if (_.isUndefined(newTags)) {
+							newTags = tags;
+						}
+						context.fileInfoModel.set({
+							'tags': newTags,
+							'favorite': !isFavorite
+						});
+					});
+				}
+			});
+		},
+
+		_extendFileList: function (fileList) {
+			// extend row prototype
+			var oldCreateRow = fileList._createRow;
+			fileList._createRow = function (fileData) {
+				var $tr = oldCreateRow.apply(this, arguments);
+				var isFavorite = false;
+				if (fileData.tags) {
+					$tr.attr('data-tags', fileData.tags.join('|'));
+					if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) {
+						$tr.attr('data-favorite', true);
+						isFavorite = true;
+					}
+				}
+				var $icon = $(renderStar(isFavorite));
+				$tr.find('td.filename .thumbnail').append($icon);
+				return $tr;
+			};
+			var oldElementToFile = fileList.elementToFile;
+			fileList.elementToFile = function ($el) {
+				var fileInfo = oldElementToFile.apply(this, arguments);
+				var tags = $el.attr('data-tags');
+				if (_.isUndefined(tags)) {
+					tags = '';
+				}
+				tags = tags.split('|');
+				tags = _.without(tags, '');
+				fileInfo.tags = tags;
+				return fileInfo;
+			};
+
+			var oldGetWebdavProperties = fileList._getWebdavProperties;
+			fileList._getWebdavProperties = function () {
+				var props = oldGetWebdavProperties.apply(this, arguments);
+				props.push(OC.Files.Client.PROPERTY_TAGS);
+				props.push(OC.Files.Client.PROPERTY_FAVORITE);
+				return props;
+			};
+
+			fileList.filesClient.addFileInfoParser(function (response) {
+				var data = {};
+				var props = response.propStat[0].properties;
+				var tags = props[OC.Files.Client.PROPERTY_TAGS];
+				var favorite = props[OC.Files.Client.PROPERTY_FAVORITE];
+				if (tags && tags.length) {
+					tags = _.chain(tags).filter(function (xmlvalue) {
+						return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'tag');
+					}).map(function (xmlvalue) {
+						return xmlvalue.textContent || xmlvalue.text;
+					}).value();
+				}
+				if (tags) {
+					data.tags = tags;
+				}
+				if (favorite && parseInt(favorite, 10) !== 0) {
+					data.tags = data.tags || [];
+					data.tags.push(OC.TAG_FAVORITE);
+				}
+				return data;
+			});
+		},
+
+		attach: function (fileList) {
+			if (this.allowedLists.indexOf(fileList.id) < 0) {
+				return;
+			}
+			this._extendFileActions(fileList.fileActions);
+			this._extendFileList(fileList);
+		},
+
+		/**
+		 * Replaces the given files' tags with the specified ones.
+		 *
+		 * @param {String} fileName path to the file or folder to tag
+		 * @param {Array.<String>} tagNames array of tag names
+		 * @param {Object} $favoriteMarkEl favorite mark element
+		 * @param {boolean} isFavorite Was the item favorited before
+		 */
+		applyFileTags: function (fileName, tagNames, $favoriteMarkEl, isFavorite) {
+			var encodedPath = OC.encodePath(fileName);
+			while (encodedPath[0] === '/') {
+				encodedPath = encodedPath.substr(1);
+			}
+			return $.ajax({
+				url: OC.generateUrl('/apps/files/api/v1/files/') + encodedPath,
+				contentType: 'application/json',
+				data: JSON.stringify({
+					tags: tagNames || []
+				}),
+				dataType: 'json',
+				type: 'POST'
+			}).fail(function (response) {
+				var message = '';
+				// show message if it is available
+				if (response.responseJSON && response.responseJSON.message) {
+					message = ': ' + response.responseJSON.message;
+				}
+				OC.Notification.show(t('files', 'An error occurred while trying to update the tags' + message), {type: 'error'});
+				toggleStar($favoriteMarkEl, isFavorite);
+			});
+		}
+	};
+})
+(OCA);
+
+OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin);
+
+
+/*
+ * Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+
+(function (OCA) {
+
+	OCA.Files = OCA.Files || {};
+
+	/**
+	 * @namespace OCA.Files.GotoPlugin
+	 *
+	 */
+	OCA.Files.GotoPlugin = {
+		name: 'Goto',
+
+		disallowedLists: [
+			'files',
+			'trashbin'
+		],
+
+		attach: function (fileList) {
+			if (this.disallowedLists.indexOf(fileList.id) !== -1) {
+				return;
+			}
+			var fileActions = fileList.fileActions;
+
+			fileActions.registerAction({
+				name: 'Goto',
+				displayName: t('files', 'View in folder'),
+				mime: 'all',
+				permissions: OC.PERMISSION_ALL,
+				iconClass: 'icon-goto nav-icon-extstoragemounts',
+				type: OCA.Files.FileActions.TYPE_DROPDOWN,
+				actionHandler: function (fileName, context) {
+					var fileModel = context.fileInfoModel;
+					OC.Apps.hideAppSidebar($('.detailsView'));
+					OCA.Files.App.setActiveView('files', {silent: true});
+					OCA.Files.App.fileList.changeDirectory(fileModel.get('path'), true, true).then(function() {
+						OCA.Files.App.fileList.scrollTo(fileModel.get('name'));
+					});
+				},
+				render: function (actionSpec, isDefault, context) {
+					return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context)
+						.removeClass('permanent');
+				}
+			});
+		}
+	};
+})(OCA);
+
+OC.Plugins.register('OCA.Files.FileList', OCA.Files.GotoPlugin);
+
+
+
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OCA) {
+	/**
+	 * Registers the favorites file list from the files app sidebar.
+	 *
+	 * @namespace OCA.Files.FavoritesPlugin
+	 */
+	OCA.Files.FavoritesPlugin = {
+		name: 'Favorites',
+
+		/**
+		 * @type OCA.Files.FavoritesFileList
+		 */
+		favoritesFileList: null,
+
+		attach: function() {
+			var self = this;
+			$('#app-content-favorites').on('show.plugin-favorites', function(e) {
+				self.showFileList($(e.target));
+			});
+			$('#app-content-favorites').on('hide.plugin-favorites', function() {
+				self.hideFileList();
+			});
+		},
+
+		detach: function() {
+			if (this.favoritesFileList) {
+				this.favoritesFileList.destroy();
+				OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated);
+				OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated);
+				$('#app-content-favorites').off('.plugin-favorites');
+				this.favoritesFileList = null;
+			}
+		},
+
+		showFileList: function($el) {
+			if (!this.favoritesFileList) {
+				this.favoritesFileList = this._createFavoritesFileList($el);
+			}
+			return this.favoritesFileList;
+		},
+
+		hideFileList: function() {
+			if (this.favoritesFileList) {
+				this.favoritesFileList.$fileList.empty();
+			}
+		},
+
+		/**
+		 * Creates the favorites file list.
+		 *
+		 * @param $el container for the file list
+		 * @return {OCA.Files.FavoritesFileList} file list
+		 */
+		_createFavoritesFileList: function($el) {
+			var fileActions = this._createFileActions();
+			// register favorite list for sidebar section
+			return new OCA.Files.FavoritesFileList(
+				$el, {
+					fileActions: fileActions,
+					// The file list is created when a "show" event is handled,
+					// so it should be marked as "shown" like it would have been
+					// done if handling the event with the file list already
+					// created.
+					shown: true
+				}
+			);
+		},
+
+		_createFileActions: function() {
+			// inherit file actions from the files app
+			var fileActions = new OCA.Files.FileActions();
+			// note: not merging the legacy actions because legacy apps are not
+			// compatible with the sharing overview and need to be adapted first
+			fileActions.registerDefaultActions();
+			fileActions.merge(OCA.Files.fileActions);
+
+			if (!this._globalActionsInitialized) {
+				// in case actions are registered later
+				this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
+				OCA.Files.fileActions.on('setDefault.plugin-favorites', this._onActionsUpdated);
+				OCA.Files.fileActions.on('registerAction.plugin-favorites', this._onActionsUpdated);
+				this._globalActionsInitialized = true;
+			}
+
+			// when the user clicks on a folder, redirect to the corresponding
+			// folder in the files app instead of opening it directly
+			fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
+				OCA.Files.App.setActiveView('files', {silent: true});
+				OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
+			});
+			fileActions.setDefault('dir', 'Open');
+			return fileActions;
+		},
+
+		_onActionsUpdated: function(ev) {
+			if (ev.action) {
+				this.favoritesFileList.fileActions.registerAction(ev.action);
+			} else if (ev.defaultAction) {
+				this.favoritesFileList.fileActions.setDefault(
+					ev.defaultAction.mime,
+					ev.defaultAction.name
+				);
+			}
+		}
+	};
+
+})(OCA);
+
+OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin);
+
+
+
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function (OCA) {
+	/**
+	 * Registers the recent file list from the files app sidebar.
+	 *
+	 * @namespace OCA.Files.RecentPlugin
+	 */
+	OCA.Files.RecentPlugin = {
+		name: 'Recent',
+
+		/**
+		 * @type OCA.Files.RecentFileList
+		 */
+		recentFileList: null,
+
+		attach: function () {
+			var self = this;
+			$('#app-content-recent').on('show.plugin-recent', function (e) {
+				self.showFileList($(e.target));
+			});
+			$('#app-content-recent').on('hide.plugin-recent', function () {
+				self.hideFileList();
+			});
+		},
+
+		detach: function () {
+			if (this.recentFileList) {
+				this.recentFileList.destroy();
+				OCA.Files.fileActions.off('setDefault.plugin-recent', this._onActionsUpdated);
+				OCA.Files.fileActions.off('registerAction.plugin-recent', this._onActionsUpdated);
+				$('#app-content-recent').off('.plugin-recent');
+				this.recentFileList = null;
+			}
+		},
+
+		showFileList: function ($el) {
+			if (!this.recentFileList) {
+				this.recentFileList = this._createRecentFileList($el);
+			}
+			return this.recentFileList;
+		},
+
+		hideFileList: function () {
+			if (this.recentFileList) {
+				this.recentFileList.$fileList.empty();
+			}
+		},
+
+		/**
+		 * Creates the recent file list.
+		 *
+		 * @param $el container for the file list
+		 * @return {OCA.Files.RecentFileList} file list
+		 */
+		_createRecentFileList: function ($el) {
+			var fileActions = this._createFileActions();
+			// register recent list for sidebar section
+			return new OCA.Files.RecentFileList(
+				$el, {
+					fileActions: fileActions,
+					// The file list is created when a "show" event is handled,
+					// so it should be marked as "shown" like it would have been
+					// done if handling the event with the file list already
+					// created.
+					shown: true
+				}
+			);
+		},
+
+		_createFileActions: function () {
+			// inherit file actions from the files app
+			var fileActions = new OCA.Files.FileActions();
+			// note: not merging the legacy actions because legacy apps are not
+			// compatible with the sharing overview and need to be adapted first
+			fileActions.registerDefaultActions();
+			fileActions.merge(OCA.Files.fileActions);
+
+			if (!this._globalActionsInitialized) {
+				// in case actions are registered later
+				this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
+				OCA.Files.fileActions.on('setDefault.plugin-recent', this._onActionsUpdated);
+				OCA.Files.fileActions.on('registerAction.plugin-recent', this._onActionsUpdated);
+				this._globalActionsInitialized = true;
+			}
+
+			// when the user clicks on a folder, redirect to the corresponding
+			// folder in the files app instead of opening it directly
+			fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
+				OCA.Files.App.setActiveView('files', {silent: true});
+				var path = OC.joinPaths(context.$file.attr('data-path'), filename);
+				OCA.Files.App.fileList.changeDirectory(path, true, true);
+			});
+			fileActions.setDefault('dir', 'Open');
+			return fileActions;
+		},
+
+		_onActionsUpdated: function (ev) {
+			if (ev.action) {
+				this.recentFileList.fileActions.registerAction(ev.action);
+			} else if (ev.defaultAction) {
+				this.recentFileList.fileActions.setDefault(
+					ev.defaultAction.mime,
+					ev.defaultAction.name
+				);
+			}
+		}
+	};
+
+})(OCA);
+
+OC.Plugins.register('OCA.Files.App', OCA.Files.RecentPlugin);
+
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	/**
+	 * @class OCA.Files.DetailFileInfoView
+	 * @classdesc
+	 *
+	 * Displays a block of details about the file info.
+	 *
+	 */
+	var DetailFileInfoView = OC.Backbone.View.extend({
+		tagName: 'div',
+		className: 'detailFileInfoView',
+
+		_template: null,
+
+		/**
+		 * returns the jQuery object for HTML output
+		 *
+		 * @returns {jQuery}
+		 */
+		get$: function() {
+			return this.$el;
+		},
+
+		/**
+		 * Sets the file info to be displayed in the view
+		 *
+		 * @param {OCA.Files.FileInfo} fileInfo file info to set
+		 */
+		setFileInfo: function(fileInfo) {
+			this.model = fileInfo;
+			this.render();
+		},
+
+		/**
+		 * Returns the file info.
+		 *
+		 * @return {OCA.Files.FileInfo} file info
+		 */
+		getFileInfo: function() {
+			return this.model;
+		}
+	});
+
+	OCA.Files.DetailFileInfoView = DetailFileInfoView;
+})();
+
+
+
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function () {
+	var SidebarPreviewManager = function (fileList) {
+		this._fileList = fileList;
+		this._previewHandlers = {};
+		OC.Plugins.attach('OCA.Files.SidebarPreviewManager', this);
+	};
+
+	SidebarPreviewManager.prototype = {
+		addPreviewHandler: function (mime, handler) {
+			this._previewHandlers[mime] = handler;
+		},
+
+		getMimeTypePreviewHandler: function(mime) {
+			var mimePart = mime.split('/').shift();
+			if (this._previewHandlers[mime]) {
+				return this._previewHandlers[mime];
+			} else if (this._previewHandlers[mimePart]) {
+				return this._previewHandlers[mimePart];
+			} else {
+				return null;
+			}
+		},
+
+		getPreviewHandler: function (mime) {
+			var mimetypeHandler = this.getMimeTypePreviewHandler(mime);
+			if (mimetypeHandler) {
+				return mimetypeHandler;
+			} else {
+				return this.fallbackPreview.bind(this);
+			}
+		},
+
+		loadPreview: function (model, $thumbnailDiv, $thumbnailContainer) {
+			if (model.get('hasPreview') === false && this.getMimeTypePreviewHandler(model.get('mimetype')) === null) {
+				var mimeIcon = OC.MimeType.getIconUrl(model.get('mimetype'));
+				$thumbnailDiv.removeClass('icon-loading icon-32');
+				$thumbnailContainer.removeClass('image'); //fall back to regular view
+				$thumbnailDiv.css({
+					'background-image': 'url("' + mimeIcon + '")'
+				});
+			} else {
+				var handler = this.getPreviewHandler(model.get('mimetype'));
+				var fallback = this.fallbackPreview.bind(this, model, $thumbnailDiv, $thumbnailContainer);
+				handler(model, $thumbnailDiv, $thumbnailContainer, fallback);
+			}
+		},
+
+		// previews for images and mimetype icons
+		fallbackPreview: function (model, $thumbnailDiv, $thumbnailContainer) {
+			var isImage = model.isImage();
+			var maxImageWidth = $thumbnailContainer.parent().width() + 50;  // 50px for negative margins
+			var maxImageHeight = maxImageWidth / (16 / 9);
+
+			var isLandscape = function (img) {
+				return img.width > (img.height * 1.2);
+			};
+
+			var isSmall = function (img) {
+				return (img.width * 1.1) < (maxImageWidth * window.devicePixelRatio);
+			};
+
+			var getTargetHeight = function (img) {
+				var targetHeight = img.height / window.devicePixelRatio;
+				if (targetHeight <= maxImageHeight) {
+					targetHeight = maxImageHeight;
+				}
+				return targetHeight;
+			};
+
+			var getTargetRatio = function (img) {
+				var ratio = img.width / img.height;
+				if (ratio > 16 / 9) {
+					return ratio;
+				} else {
+					return 16 / 9;
+				}
+			};
+
+			this._fileList.lazyLoadPreview({
+				fileId: model.get('id'),
+				path: model.getFullPath(),
+				mime: model.get('mimetype'),
+				etag: model.get('etag'),
+				y: maxImageHeight,
+				x: maxImageWidth,
+				a: 1,
+				mode: 'cover',
+				callback: function (previewUrl, img) {
+					$thumbnailDiv.previewImg = previewUrl;
+
+					// as long as we only have the mimetype icon, we only save it in case there is no preview
+					if (!img) {
+						return;
+					}
+					$thumbnailDiv.removeClass('icon-loading icon-32');
+					var targetHeight = getTargetHeight(img);
+					$thumbnailContainer.addClass((isLandscape(img) && !isSmall(img)) ? 'landscape' : 'portrait');
+					$thumbnailContainer.addClass('large');
+
+					// only set background when we have an actual preview
+					// when we don't have a preview we show the mime icon in the error handler
+					$thumbnailDiv.css({
+						'background-image': 'url("' + previewUrl + '")',
+						height: (targetHeight > maxImageHeight) ? 'auto' : targetHeight,
+						'max-height': isSmall(img) ? targetHeight : null
+					});
+
+					var targetRatio = getTargetRatio(img);
+					$thumbnailDiv.find('.stretcher').css({
+						'padding-bottom': (100 / targetRatio) + '%'
+					});
+				},
+				error: function () {
+					$thumbnailDiv.removeClass('icon-loading icon-32');
+					$thumbnailContainer.removeClass('image'); //fall back to regular view
+					$thumbnailDiv.css({
+						'background-image': 'url("' + $thumbnailDiv.previewImg + '")'
+					});
+				}
+			});
+		}
+	};
+
+	OCA.Files.SidebarPreviewManager = SidebarPreviewManager;
+})();
+
+
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function () {
+	var SidebarPreview = function () {
+	};
+
+	SidebarPreview.prototype = {
+		attach: function (manager) {
+			manager.addPreviewHandler('text', this.handlePreview.bind(this));
+		},
+
+		handlePreview: function (model, $thumbnailDiv, $thumbnailContainer, fallback) {
+			var previewWidth = $thumbnailContainer.parent().width() + 50;  // 50px for negative margins
+			var previewHeight = previewWidth / (16 / 9);
+
+			this.getFileContent(model.getFullPath()).then(function (content) {
+				$thumbnailDiv.removeClass('icon-loading icon-32');
+				$thumbnailContainer.addClass('large');
+				$thumbnailContainer.addClass('text');
+				var $textPreview = $('<pre/>').text(content);
+				$thumbnailDiv.children('.stretcher').remove();
+				$thumbnailDiv.append($textPreview);
+				$thumbnailContainer.css("max-height", previewHeight);
+			}, function () {
+				fallback();
+			});
+		},
+
+		getFileContent: function (path) {
+			return $.ajax({
+				url: OC.linkToRemoteBase('files' + path),
+				headers: {
+					'Range': 'bytes=0-10240'
+				}
+			});
+		}
+	};
+
+	OC.Plugins.register('OCA.Files.SidebarPreviewManager', new SidebarPreview());
+})();
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	/**
+	 * @class OCA.Files.DetailTabView
+	 * @classdesc
+	 *
+	 * Base class for tab views to display file information.
+	 *
+	 */
+	var DetailTabView = OC.Backbone.View.extend({
+		tag: 'div',
+
+		className: 'tab',
+
+		/**
+		 * Tab label
+		 */
+		_label: null,
+
+		_template: null,
+
+		initialize: function(options) {
+			options = options || {};
+			if (!this.id) {
+				this.id = 'detailTabView' + DetailTabView._TAB_COUNT;
+				DetailTabView._TAB_COUNT++;
+			}
+			if (options.order) {
+				this.order = options.order || 0;
+			}
+		},
+
+		/**
+		 * Returns the extra CSS classes used by the tabs container when this
+		 * tab is the selected one.
+		 *
+		 * In general you should not extend this method, as tabs should not
+		 * modify the classes of its container; this is reserved as a last
+		 * resort for very specific cases in which there is no other way to get
+		 * the proper style or behaviour.
+		 *
+		 * @return {String} space-separated CSS classes
+		 */
+		getTabsContainerExtraClasses: function() {
+			return '';
+		},
+
+		/**
+		 * Returns the tab label
+		 *
+		 * @return {String} label
+		 */
+		getLabel: function() {
+			return 'Tab ' + this.id;
+		},
+
+		/**
+		 * Returns the tab label
+		 *
+		 * @return {String}|{null} icon class
+		 */
+		getIcon: function() {
+			return null
+		},
+
+		/**
+		 * returns the jQuery object for HTML output
+		 *
+		 * @returns {jQuery}
+		 */
+		get$: function() {
+			return this.$el;
+		},
+
+		/**
+		 * Renders this details view
+		 *
+		 * @abstract
+		 */
+		render: function() {
+			// to be implemented in subclass
+			// FIXME: code is only for testing
+			this.$el.html('<div>Hello ' + this.id + '</div>');
+		},
+
+		/**
+		 * Sets the file info to be displayed in the view
+		 *
+		 * @param {OCA.Files.FileInfoModel} fileInfo file info to set
+		 */
+		setFileInfo: function(fileInfo) {
+			if (this.model !== fileInfo) {
+				this.model = fileInfo;
+				this.render();
+			}
+		},
+
+		/**
+		 * Returns the file info.
+		 *
+		 * @return {OCA.Files.FileInfoModel} file info
+		 */
+		getFileInfo: function() {
+			return this.model;
+		},
+
+		/**
+		 * Load the next page of results
+		 */
+		nextPage: function() {
+			// load the next page, if applicable
+		},
+
+		/**
+		 * Returns whether the current tab is able to display
+		 * the given file info, for example based on mime type.
+		 *
+		 * @param {OCA.Files.FileInfoModel} fileInfo file info model
+		 * @return {bool} whether to display this tab
+		 */
+		canDisplay: function(fileInfo) {
+			return true;
+		}
+	});
+	DetailTabView._TAB_COUNT = 0;
+
+	OCA.Files = OCA.Files || {};
+
+	OCA.Files.DetailTabView = DetailTabView;
+})();
+
+
+
+/*
+ * Copyright (c) 2018
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(){
+	var Semaphore = function(max) {
+		var counter = 0;
+		var waiting = [];
+
+		this.acquire = function() {
+			if(counter < max) {
+				counter++;
+				return new Promise(function(resolve) { resolve(); });
+			} else {
+				return new Promise(function(resolve) { waiting.push(resolve); });
+			}
+		};
+
+		this.release = function() {
+			counter--;
+			if (waiting.length > 0 && counter < max) {
+				counter++;
+				var promise = waiting.shift();
+				promise();
+			}
+		};
+	};
+
+	// needed on public share page to properly register this
+	if (!OCA.Files) {
+		OCA.Files = {};
+	}
+	OCA.Files.Semaphore = Semaphore;
+
+})();
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	/**
+	 * @class OCA.Files.MainFileInfoDetailView
+	 * @classdesc
+	 *
+	 * Displays main details about a file
+	 *
+	 */
+	var MainFileInfoDetailView = OCA.Files.DetailFileInfoView.extend(
+		/** @lends OCA.Files.MainFileInfoDetailView.prototype */ {
+
+		className: 'mainFileInfoView',
+
+		/**
+		 * Associated file list instance, for file actions
+		 *
+		 * @type {OCA.Files.FileList}
+		 */
+		_fileList: null,
+
+		/**
+		 * File actions
+		 *
+		 * @type {OCA.Files.FileActions}
+		 */
+		_fileActions: null,
+
+		/**
+		 * @type {OCA.Files.SidebarPreviewManager}
+		 */
+		_previewManager: null,
+
+		events: {
+			'click a.action-favorite': '_onClickFavorite',
+			'click a.action-default': '_onClickDefaultAction',
+			'click a.permalink': '_onClickPermalink',
+			'focus .permalink-field>input': '_onFocusPermalink'
+		},
+
+		template: function(data) {
+			return OCA.Files.Templates['mainfileinfodetailsview'](data);
+		},
+
+		initialize: function(options) {
+			options = options || {};
+			this._fileList = options.fileList;
+			this._fileActions = options.fileActions;
+			if (!this._fileList) {
+				throw 'Missing required parameter "fileList"';
+			}
+			if (!this._fileActions) {
+				throw 'Missing required parameter "fileActions"';
+			}
+			this._previewManager = new OCA.Files.SidebarPreviewManager(this._fileList);
+
+			this._setupClipboard();
+		},
+
+		_setupClipboard: function() {
+			var clipboard = new Clipboard('.permalink');
+			clipboard.on('success', function(e) {
+				var $el = $(e.trigger);
+				$el.tooltip('hide')
+					.attr('data-original-title', t('core', 'Copied!'))
+					.tooltip('fixTitle')
+					.tooltip({placement: 'bottom', trigger: 'manual'})
+					.tooltip('show');
+				_.delay(function() {
+					$el.tooltip('hide');
+					$el.attr('data-original-title', t('files', 'Copy direct link (only works for users who have access to this file/folder)'))
+						.tooltip('fixTitle');
+				}, 3000);
+			});
+			clipboard.on('error', function(e) {
+				var $row = this.$('.permalink-field');
+				$row.toggleClass('hidden');
+				if (!$row.hasClass('hidden')) {
+					$row.find('>input').focus();
+				}
+			});
+		},
+
+		_onClickPermalink: function(e) {
+			e.preventDefault();
+			return;
+		},
+
+		_onFocusPermalink: function() {
+			this.$('.permalink-field>input').select();
+		},
+
+		_onClickFavorite: function(event) {
+			event.preventDefault();
+			this._fileActions.triggerAction('Favorite', this.model, this._fileList);
+		},
+
+		_onClickDefaultAction: function(event) {
+			event.preventDefault();
+			this._fileActions.triggerAction(null, this.model, this._fileList);
+		},
+
+		_onModelChanged: function() {
+			// simply re-render
+			this.render();
+		},
+
+		_makePermalink: function(fileId) {
+			var baseUrl = OC.getProtocol() + '://' + OC.getHost();
+			return baseUrl + OC.generateUrl('/f/{fileId}', {fileId: fileId});
+		},
+
+		setFileInfo: function(fileInfo) {
+			if (this.model) {
+				this.model.off('change', this._onModelChanged, this);
+			}
+			this.model = fileInfo;
+			if (this.model) {
+				this.model.on('change', this._onModelChanged, this);
+			}
+
+			if (this.model) {
+				var properties = [];
+				if( !this.model.has('size') ) {
+					properties.push(OC.Files.Client.PROPERTY_SIZE);
+					properties.push(OC.Files.Client.PROPERTY_GETCONTENTLENGTH);
+				}
+
+				if( properties.length > 0){
+					this.model.reloadProperties(properties);
+				}
+			}
+
+			this.render();
+		},
+
+		/**
+		 * Renders this details view
+		 */
+		render: function() {
+			this.trigger('pre-render');
+
+			if (this.model) {
+				var isFavorite = (this.model.get('tags') || []).indexOf(OC.TAG_FAVORITE) >= 0;
+				var availableActions = this._fileActions.get(
+					this.model.get('mimetype'),
+					this.model.get('type'),
+					this.model.get('permissions')
+				);
+				var hasFavoriteAction = 'Favorite' in availableActions;
+				this.$el.html(this.template({
+					type: this.model.isImage()? 'image': '',
+					nameLabel: t('files', 'Name'),
+					name: this.model.get('displayName') || this.model.get('name'),
+					pathLabel: t('files', 'Path'),
+					path: this.model.get('path'),
+					hasSize: this.model.has('size'),
+					sizeLabel: t('files', 'Size'),
+					size: OC.Util.humanFileSize(this.model.get('size'), true),
+					altSize: n('files', '%n byte', '%n bytes', this.model.get('size')),
+					dateLabel: t('files', 'Modified'),
+					altDate: OC.Util.formatDate(this.model.get('mtime')),
+					timestamp: this.model.get('mtime'),
+					date: OC.Util.relativeModifiedDate(this.model.get('mtime')),
+					hasFavoriteAction: hasFavoriteAction,
+					starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'),
+					starClass: isFavorite ? 'icon-starred' : 'icon-star',
+					permalink: this._makePermalink(this.model.get('id')),
+					permalinkTitle: t('files', 'Copy direct link (only works for users who have access to this file/folder)')
+				}));
+
+				// TODO: we really need OC.Previews
+				var $iconDiv = this.$el.find('.thumbnail');
+				var $container = this.$el.find('.thumbnailContainer');
+				if (!this.model.isDirectory()) {
+					$iconDiv.addClass('icon-loading icon-32');
+					this._previewManager.loadPreview(this.model, $iconDiv, $container);
+				} else {
+					var iconUrl = this.model.get('icon') || OC.MimeType.getIconUrl('dir');
+					$iconDiv.css('background-image', 'url("' + iconUrl + '")');
+				}
+				this.$el.find('[title]').tooltip({placement: 'bottom'});
+			} else {
+				this.$el.empty();
+			}
+			this.delegateEvents();
+
+			this.trigger('post-render');
+		}
+	});
+
+	OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView;
+})();
+
+
+/*
+ * Copyright (c) 2018
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	var OperationProgressBar = OC.Backbone.View.extend({
+		tagName: 'div',
+		id: 'uploadprogresswrapper',
+		events: {
+			'click button.stop': '_onClickCancel'
+		},
+
+		render: function() {
+			this.$el.html(OCA.Files.Templates['operationprogressbar']({
+				textCancelButton: t('Cancel operation')
+			}));
+			this.setProgressBarText(t('Uploading …'), t('…'));
+		},
+
+		hideProgressBar: function() {
+			var self = this;
+			$('#uploadprogresswrapper .stop').fadeOut();
+			$('#uploadprogressbar').fadeOut(function() {
+				self.$el.trigger(new $.Event('resized'));
+			});
+		},
+
+		hideCancelButton: function() {
+			var self = this;
+			$('#uploadprogresswrapper .stop').fadeOut(function() {
+				self.$el.trigger(new $.Event('resized'));
+			});
+		},
+
+		showProgressBar: function(showCancelButton) {
+			if (showCancelButton) {
+				showCancelButton = true;
+			}
+			$('#uploadprogressbar').progressbar({value: 0});
+			if(showCancelButton) {
+				$('#uploadprogresswrapper .stop').show();
+			} else {
+				$('#uploadprogresswrapper .stop').hide();
+			}
+			$('#uploadprogresswrapper .label').show();
+			$('#uploadprogressbar').fadeIn();
+			this.$el.trigger(new $.Event('resized'));
+		},
+
+		setProgressBarValue: function(value) {
+			$('#uploadprogressbar').progressbar({value: value});
+		},
+
+		setProgressBarText: function(textDesktop, textMobile, title) {
+			var labelHtml = OCA.Files.Templates['operationprogressbarlabel']({textDesktop: textDesktop, textMobile: textMobile});
+			$('#uploadprogressbar .ui-progressbar-value').html(labelHtml);
+			$('#uploadprogressbar .ui-progressbar-value>em').addClass('inner');
+			$('#uploadprogressbar>em').replaceWith(labelHtml);
+			$('#uploadprogressbar>em').addClass('outer');
+			$('#uploadprogressbar').tooltip({placement: 'bottom'});
+			if(title) {
+				$('#uploadprogressbar').attr('original-title', title);
+			}
+			if(textDesktop || textMobile) {
+				$('#uploadprogresswrapper .stop').show();
+			}
+		},
+
+		_onClickCancel: function (event) {
+			this.trigger('cancel');
+			return false;
+		}
+	});
+
+	OCA.Files.OperationProgressBar = OperationProgressBar;
+})(OC, OCA);
+
+
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+	/**
+	 * @class OCA.Files.DetailsView
+	 * @classdesc
+	 *
+	 * The details view show details about a selected file.
+	 *
+	 */
+	var DetailsView = OC.Backbone.View.extend({
+		id: 'app-sidebar',
+		tabName: 'div',
+		className: 'detailsView scroll-container',
+
+		/**
+		 * List of detail tab views
+		 *
+		 * @type Array<OCA.Files.DetailTabView>
+		 */
+		_tabViews: [],
+
+		/**
+		 * List of detail file info views
+		 *
+		 * @type Array<OCA.Files.DetailFileInfoView>
+		 */
+		_detailFileInfoViews: [],
+
+		/**
+		 * Id of the currently selected tab
+		 *
+		 * @type string
+		 */
+		_currentTabId: null,
+
+		/**
+		 * Dirty flag, whether the view needs to be rerendered
+		 */
+		_dirty: false,
+
+		events: {
+			'click a.close': '_onClose',
+			'click .tabHeaders .tabHeader': '_onClickTab',
+			'keyup .tabHeaders .tabHeader': '_onKeyboardActivateTab'
+		},
+
+		/**
+		 * Initialize the details view
+		 */
+		initialize: function() {
+			this._tabViews = [];
+			this._detailFileInfoViews = [];
+
+			this._dirty = true;
+		},
+
+		_onClose: function(event) {
+			OC.Apps.hideAppSidebar(this.$el);
+			event.preventDefault();
+		},
+
+		_onClickTab: function(e) {
+			var $target = $(e.target);
+			e.preventDefault();
+			if (!$target.hasClass('tabHeader')) {
+				$target = $target.closest('.tabHeader');
+			}
+			var tabId = $target.attr('data-tabid');
+			if (_.isUndefined(tabId)) {
+				return;
+			}
+
+			this.selectTab(tabId);
+		},
+
+		_onKeyboardActivateTab: function (event) {
+			if (event.key === " " || event.key === "Enter") {
+				this._onClickTab(event);
+			}
+		},
+
+		template: function(vars) {
+			return OCA.Files.Templates['detailsview'](vars);
+		},
+
+		/**
+		 * Renders this details view
+		 */
+		render: function() {
+			var templateVars = {
+				closeLabel: t('files', 'Close')
+			};
+
+			this._tabViews = this._tabViews.sort(function(tabA, tabB) {
+				var orderA = tabA.order || 0;
+				var orderB = tabB.order || 0;
+				if (orderA === orderB) {
+					return OC.Util.naturalSortCompare(tabA.getLabel(), tabB.getLabel());
+				}
+				return orderA - orderB;
+			});
+
+			templateVars.tabHeaders = _.map(this._tabViews, function(tabView, i) {
+				return {
+					tabId: tabView.id,
+					label: tabView.getLabel(),
+					tabIcon: tabView.getIcon()
+				};
+			});
+
+			this.$el.html(this.template(templateVars));
+
+			var $detailsContainer = this.$el.find('.detailFileInfoContainer');
+
+			// render details
+			_.each(this._detailFileInfoViews, function(detailView) {
+				$detailsContainer.append(detailView.get$());
+			});
+
+			if (!this._currentTabId && this._tabViews.length > 0) {
+				this._currentTabId = this._tabViews[0].id;
+			}
+
+			this.selectTab(this._currentTabId);
+
+			this._updateTabVisibilities();
+
+			this._dirty = false;
+		},
+
+		/**
+		 * Selects the given tab by id
+		 *
+		 * @param {string} tabId tab id
+		 */
+		selectTab: function(tabId) {
+			if (!tabId) {
+				return;
+			}
+
+			var tabView = _.find(this._tabViews, function(tab) {
+				return tab.id === tabId;
+			});
+
+			if (!tabView) {
+				console.warn('Details view tab with id "' + tabId + '" not found');
+				return;
+			}
+
+			this._currentTabId = tabId;
+
+			var $tabsContainer = this.$el.find('.tabsContainer');
+			var $tabEl = $tabsContainer.find('#' + tabId);
+
+			// hide other tabs
+			$tabsContainer.find('.tab').addClass('hidden');
+
+			$tabsContainer.attr('class', 'tabsContainer');
+			$tabsContainer.addClass(tabView.getTabsContainerExtraClasses());
+
+			// tab already rendered ?
+			if (!$tabEl.length) {
+				// render tab
+				$tabsContainer.append(tabView.$el);
+				$tabEl = tabView.$el;
+			}
+
+			// this should trigger tab rendering
+			tabView.setFileInfo(this.model);
+
+			$tabEl.removeClass('hidden');
+
+			// update tab headers
+			var $tabHeaders = this.$el.find('.tabHeaders li');
+			$tabHeaders.removeClass('selected');
+			$tabHeaders.filterAttr('data-tabid', tabView.id).addClass('selected');
+		},
+
+		/**
+		 * Sets the file info to be displayed in the view
+		 *
+		 * @param {OCA.Files.FileInfoModel} fileInfo file info to set
+		 */
+		setFileInfo: function(fileInfo) {
+			this.model = fileInfo;
+
+			if (this._dirty) {
+				this.render();
+			} else {
+				this._updateTabVisibilities();
+			}
+
+			if (this._currentTabId) {
+				// only update current tab, others will be updated on-demand
+				var tabId = this._currentTabId;
+				var tabView = _.find(this._tabViews, function(tab) {
+					return tab.id === tabId;
+				});
+				tabView.setFileInfo(fileInfo);
+			}
+
+			_.each(this._detailFileInfoViews, function(detailView) {
+				detailView.setFileInfo(fileInfo);
+			});
+		},
+
+		/**
+		 * Update tab headers based on the current model
+		 */
+		_updateTabVisibilities: function() {
+			// update tab header visibilities
+			var self = this;
+			var deselect = false;
+			var countVisible = 0;
+			var $tabHeaders = this.$el.find('.tabHeaders li');
+			_.each(this._tabViews, function(tabView) {
+				var isVisible = tabView.canDisplay(self.model);
+				if (isVisible) {
+					countVisible += 1;
+				}
+				if (!isVisible && self._currentTabId === tabView.id) {
+					deselect = true;
+				}
+				$tabHeaders.filterAttr('data-tabid', tabView.id).toggleClass('hidden', !isVisible);
+			});
+
+			// hide the whole container if there is only one tab
+			this.$el.find('.tabHeaders').toggleClass('hidden', countVisible <= 1);
+
+			if (deselect) {
+				// select the first visible tab instead
+				var visibleTabId = this.$el.find('.tabHeader:not(.hidden):first').attr('data-tabid');
+				this.selectTab(visibleTabId);
+			}
+
+		},
+
+		/**
+		 * Returns the file info.
+		 *
+		 * @return {OCA.Files.FileInfoModel} file info
+		 */
+		getFileInfo: function() {
+			return this.model;
+		},
+
+		/**
+		 * Adds a tab in the tab view
+		 *
+		 * @param {OCA.Files.DetailTabView} tab view
+		 */
+		addTabView: function(tabView) {
+			this._tabViews.push(tabView);
+			this._dirty = true;
+		},
+
+		/**
+		 * Adds a detail view for file info.
+		 *
+		 * @param {OCA.Files.DetailFileInfoView} detail view
+		 */
+		addDetailView: function(detailView) {
+			this._detailFileInfoViews.push(detailView);
+			this._dirty = true;
+		},
+
+		/**
+		 * Returns an array with the added DetailFileInfoViews.
+		 *
+		 * @return Array<OCA.Files.DetailFileInfoView> an array with the added
+		 *         DetailFileInfoViews.
+		 */
+		getDetailViews: function() {
+			return [].concat(this._detailFileInfoViews);
+		}
+	});
+
+	OCA.Files.DetailsView = DetailsView;
+})();
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	/**
+	 * Construct a new FileActions instance
+	 * @constructs FileActions
+	 * @memberof OCA.Files
+	 */
+	var FileActions = function() {
+		this.initialize();
+	};
+	FileActions.TYPE_DROPDOWN = 0;
+	FileActions.TYPE_INLINE = 1;
+	FileActions.prototype = {
+		/** @lends FileActions.prototype */
+		actions: {},
+		defaults: {},
+		icons: {},
+
+		/**
+		 * @deprecated
+		 */
+		currentFile: null,
+
+		/**
+		 * Dummy jquery element, for events
+		 */
+		$el: null,
+
+		_fileActionTriggerTemplate: null,
+
+		/**
+		 * @private
+		 */
+		initialize: function() {
+			this.clear();
+			// abusing jquery for events until we get a real event lib
+			this.$el = $('<div class="dummy-fileactions hidden"></div>');
+			$('body').append(this.$el);
+
+			this._showMenuClosure = _.bind(this._showMenu, this);
+		},
+
+		/**
+		 * Adds an event handler
+		 *
+		 * @param {String} eventName event name
+		 * @param {Function} callback
+		 */
+		on: function(eventName, callback) {
+			this.$el.on(eventName, callback);
+		},
+
+		/**
+		 * Removes an event handler
+		 *
+		 * @param {String} eventName event name
+		 * @param {Function} callback
+		 */
+		off: function(eventName, callback) {
+			this.$el.off(eventName, callback);
+		},
+
+		/**
+		 * Notifies the event handlers
+		 *
+		 * @param {String} eventName event name
+		 * @param {Object} data data
+		 */
+		_notifyUpdateListeners: function(eventName, data) {
+			this.$el.trigger(new $.Event(eventName, data));
+		},
+
+		/**
+		 * Merges the actions from the given fileActions into
+		 * this instance.
+		 *
+		 * @param {OCA.Files.FileActions} fileActions instance of OCA.Files.FileActions
+		 */
+		merge: function(fileActions) {
+			var self = this;
+			// merge first level to avoid unintended overwriting
+			_.each(fileActions.actions, function(sourceMimeData, mime) {
+				var targetMimeData = self.actions[mime];
+				if (!targetMimeData) {
+					targetMimeData = {};
+				}
+				self.actions[mime] = _.extend(targetMimeData, sourceMimeData);
+			});
+
+			this.defaults = _.extend(this.defaults, fileActions.defaults);
+			this.icons = _.extend(this.icons, fileActions.icons);
+		},
+		/**
+		 * @deprecated use #registerAction() instead
+		 */
+		register: function(mime, name, permissions, icon, action, displayName) {
+			return this.registerAction({
+				name: name,
+				mime: mime,
+				permissions: permissions,
+				icon: icon,
+				actionHandler: action,
+				displayName: displayName || name
+			});
+		},
+
+		/**
+		 * Register action
+		 *
+		 * @param {OCA.Files.FileAction} action object
+		 */
+		registerAction: function (action) {
+			var mime = action.mime;
+			var name = action.name;
+			var actionSpec = {
+				action: function(fileName, context) {
+					// Actions registered in one FileAction may be executed on a
+					// different one (for example, due to the "merge" function),
+					// so the listeners have to be updated on the FileActions
+					// from the context instead of on the one in which it was
+					// originally registered.
+					if (context && context.fileActions) {
+						context.fileActions._notifyUpdateListeners('beforeTriggerAction', {action: actionSpec, fileName: fileName, context: context});
+					}
+
+					action.actionHandler(fileName, context);
+
+					if (context && context.fileActions) {
+						context.fileActions._notifyUpdateListeners('afterTriggerAction', {action: actionSpec, fileName: fileName, context: context});
+					}
+				},
+				name: name,
+				displayName: action.displayName,
+				mime: mime,
+				order: action.order || 0,
+				icon: action.icon,
+				iconClass: action.iconClass,
+				permissions: action.permissions,
+				type: action.type || FileActions.TYPE_DROPDOWN,
+				altText: action.altText || ''
+			};
+			if (_.isUndefined(action.displayName)) {
+				actionSpec.displayName = t('files', name);
+			}
+			if (_.isFunction(action.render)) {
+				actionSpec.render = action.render;
+			}
+			if (!this.actions[mime]) {
+				this.actions[mime] = {};
+			}
+			this.actions[mime][name] = actionSpec;
+			this.icons[name] = action.icon;
+			this._notifyUpdateListeners('registerAction', {action: action});
+		},
+		/**
+		 * Clears all registered file actions.
+		 */
+		clear: function() {
+			this.actions = {};
+			this.defaults = {};
+			this.icons = {};
+			this.currentFile = null;
+		},
+		/**
+		 * Sets the default action for a given mime type.
+		 *
+		 * @param {String} mime mime type
+		 * @param {String} name action name
+		 */
+		setDefault: function (mime, name) {
+			this.defaults[mime] = name;
+			this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}});
+		},
+
+		/**
+		 * Returns a map of file actions handlers matching the given conditions
+		 *
+		 * @param {string} mime mime type
+		 * @param {string} type "dir" or "file"
+		 * @param {int} permissions permissions
+		 *
+		 * @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec
+		 */
+		get: function (mime, type, permissions) {
+			var actions = this.getActions(mime, type, permissions);
+			var filteredActions = {};
+			$.each(actions, function (name, action) {
+				filteredActions[name] = action.action;
+			});
+			return filteredActions;
+		},
+
+		/**
+		 * Returns an array of file actions matching the given conditions
+		 *
+		 * @param {string} mime mime type
+		 * @param {string} type "dir" or "file"
+		 * @param {int} permissions permissions
+		 *
+		 * @return {Array.<OCA.Files.FileAction>} array of action specs
+		 */
+		getActions: function (mime, type, permissions) {
+			var actions = {};
+			if (this.actions.all) {
+				actions = $.extend(actions, this.actions.all);
+			}
+			if (type) {//type is 'dir' or 'file'
+				if (this.actions[type]) {
+					actions = $.extend(actions, this.actions[type]);
+				}
+			}
+			if (mime) {
+				var mimePart = mime.substr(0, mime.indexOf('/'));
+				if (this.actions[mimePart]) {
+					actions = $.extend(actions, this.actions[mimePart]);
+				}
+				if (this.actions[mime]) {
+					actions = $.extend(actions, this.actions[mime]);
+				}
+			}
+			var filteredActions = {};
+			$.each(actions, function (name, action) {
+				if ((action.permissions === OC.PERMISSION_NONE) || (action.permissions & permissions)) {
+					filteredActions[name] = action;
+				}
+			});
+			return filteredActions;
+		},
+
+		/**
+		 * Returns the default file action handler for the given conditions
+		 *
+		 * @param {string} mime mime type
+		 * @param {string} type "dir" or "file"
+		 * @param {int} permissions permissions
+		 *
+		 * @return {OCA.Files.FileActions~actionHandler} action handler
+		 *
+		 * @deprecated use getDefaultFileAction instead
+		 */
+		getDefault: function (mime, type, permissions) {
+			var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions);
+			if (defaultActionSpec) {
+				return defaultActionSpec.action;
+			}
+			return undefined;
+		},
+
+		/**
+		 * Returns the default file action handler for the given conditions
+		 *
+		 * @param {string} mime mime type
+		 * @param {string} type "dir" or "file"
+		 * @param {int} permissions permissions
+		 *
+		 * @return {OCA.Files.FileActions~actionHandler} action handler
+		 * @since 8.2
+		 */
+		getDefaultFileAction: function(mime, type, permissions) {
+			var mimePart;
+			if (mime) {
+				mimePart = mime.substr(0, mime.indexOf('/'));
+			}
+			var name = false;
+			if (mime && this.defaults[mime]) {
+				name = this.defaults[mime];
+			} else if (mime && this.defaults[mimePart]) {
+				name = this.defaults[mimePart];
+			} else if (type && this.defaults[type]) {
+				name = this.defaults[type];
+			} else {
+				name = this.defaults.all;
+			}
+			var actions = this.getActions(mime, type, permissions);
+			return actions[name];
+		},
+
+		/**
+		 * Default function to render actions
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action spec
+		 * @param {boolean} isDefault true if the action is a default one,
+		 * false otherwise
+		 * @param {OCA.Files.FileActionContext} context action context
+		 */
+		_defaultRenderAction: function(actionSpec, isDefault, context) {
+			if (!isDefault) {
+				var params = {
+					name: actionSpec.name,
+					nameLowerCase: actionSpec.name.toLowerCase(),
+					displayName: actionSpec.displayName,
+					icon: actionSpec.icon,
+					iconClass: actionSpec.iconClass,
+					altText: actionSpec.altText,
+					hasDisplayName: !!actionSpec.displayName
+				};
+				if (_.isFunction(actionSpec.icon)) {
+					params.icon = actionSpec.icon(context.$file.attr('data-file'), context);
+				}
+				if (_.isFunction(actionSpec.iconClass)) {
+					params.iconClass = actionSpec.iconClass(context.$file.attr('data-file'), context);
+				}
+
+				var $actionLink = this._makeActionLink(params, context);
+				context.$file.find('a.name>span.fileactions').append($actionLink);
+				$actionLink.addClass('permanent');
+				return $actionLink;
+			}
+		},
+
+		/**
+		 * Renders the action link element
+		 *
+		 * @param {Object} params action params
+		 */
+		_makeActionLink: function(params) {
+			return $(OCA.Files.Templates['file_action_trigger'](params));
+		},
+
+		/**
+		 * Displays the file actions dropdown menu
+		 *
+		 * @param {string} fileName file name
+		 * @param {OCA.Files.FileActionContext} context rendering context
+		 */
+		_showMenu: function(fileName, context) {
+			var menu;
+			var $trigger = context.$file.closest('tr').find('.fileactions .action-menu');
+			$trigger.addClass('open');
+
+			menu = new OCA.Files.FileActionsMenu();
+
+			context.$file.find('td.filename').append(menu.$el);
+
+			menu.$el.on('afterHide', function() {
+				context.$file.removeClass('mouseOver');
+				$trigger.removeClass('open');
+				menu.remove();
+			});
+
+			context.$file.addClass('mouseOver');
+			menu.show(context);
+		},
+
+		/**
+		 * Renders the menu trigger on the given file list row
+		 *
+		 * @param {Object} $tr file list row element
+		 * @param {OCA.Files.FileActionContext} context rendering context
+		 */
+		_renderMenuTrigger: function($tr, context) {
+			// remove previous
+			$tr.find('.action-menu').remove();
+
+			var $el = this._renderInlineAction({
+				name: 'menu',
+				displayName: '',
+				iconClass: 'icon-more',
+				altText: t('files', 'Actions'),
+				action: this._showMenuClosure
+			}, false, context);
+
+			$el.addClass('permanent');
+
+		},
+
+		/**
+		 * Renders the action element by calling actionSpec.render() and
+		 * registers the click event to process the action.
+		 *
+		 * @param {OCA.Files.FileAction} actionSpec file action to render
+		 * @param {boolean} isDefault true if the action is a default action,
+		 * false otherwise
+		 * @param {OCA.Files.FileActionContext} context rendering context
+		 */
+		_renderInlineAction: function(actionSpec, isDefault, context) {
+			var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this);
+			var $actionEl = renderFunc(actionSpec, isDefault, context);
+			if (!$actionEl || !$actionEl.length) {
+				return;
+			}
+			$actionEl.on(
+				'click', {
+					a: null
+				},
+				function(event) {
+					event.stopPropagation();
+					event.preventDefault();
+
+					if ($actionEl.hasClass('open')) {
+						return;
+					}
+
+					var $file = $(event.target).closest('tr');
+					if ($file.hasClass('busy')) {
+						return;
+					}
+					var currentFile = $file.find('td.filename');
+					var fileName = $file.attr('data-file');
+
+					context.fileActions.currentFile = currentFile;
+					// also set on global object for legacy apps
+					window.FileActions.currentFile = currentFile;
+
+					var callContext = _.extend({}, context);
+
+					if (!context.dir && context.fileList) {
+						callContext.dir = $file.attr('data-path') || context.fileList.getCurrentDirectory();
+					}
+
+					if (!context.fileInfoModel && context.fileList) {
+						callContext.fileInfoModel = context.fileList.getModelForFile(fileName);
+						if (!callContext.fileInfoModel) {
+							console.warn('No file info model found for file "' + fileName + '"');
+						}
+					}
+
+					actionSpec.action(
+						fileName,
+						callContext
+					);
+				}
+			);
+			$actionEl.tooltip({placement:'top'});
+			return $actionEl;
+		},
+
+		/**
+		 * Trigger the given action on the given file.
+		 *
+		 * @param {string} actionName action name
+		 * @param {OCA.Files.FileInfoModel} fileInfoModel file info model
+		 * @param {OCA.Files.FileList} [fileList] file list, for compatibility with older action handlers [DEPRECATED]
+		 *
+		 * @return {boolean} true if the action handler was called, false otherwise
+		 *
+		 * @since 8.2
+		 */
+		triggerAction: function(actionName, fileInfoModel, fileList) {
+			var actionFunc;
+			var actions = this.get(
+				fileInfoModel.get('mimetype'),
+				fileInfoModel.isDirectory() ? 'dir' : 'file',
+				fileInfoModel.get('permissions')
+			);
+
+			if (actionName) {
+				actionFunc = actions[actionName];
+			} else {
+				actionFunc = this.getDefault(
+					fileInfoModel.get('mimetype'),
+					fileInfoModel.isDirectory() ? 'dir' : 'file',
+					fileInfoModel.get('permissions')
+				);
+			}
+
+			if (!actionFunc) {
+				actionFunc = actions['Download'];
+			}
+
+			if (!actionFunc) {
+				return false;
+			}
+
+			var context = {
+				fileActions: this,
+				fileInfoModel: fileInfoModel,
+				dir: fileInfoModel.get('path')
+			};
+
+			var fileName = fileInfoModel.get('name');
+			this.currentFile = fileName;
+			// also set on global object for legacy apps
+			window.FileActions.currentFile = fileName;
+
+			if (fileList) {
+				// compatibility with action handlers that expect these
+				context.fileList = fileList;
+				context.$file = fileList.findFileEl(fileName);
+			}
+
+			actionFunc(fileName, context);
+		},
+
+		/**
+		 * Display file actions for the given element
+		 * @param parent "td" element of the file for which to display actions
+		 * @param triggerEvent if true, triggers the fileActionsReady on the file
+		 * list afterwards (false by default)
+		 * @param fileList OCA.Files.FileList instance on which the action is
+		 * done, defaults to OCA.Files.App.fileList
+		 */
+		display: function (parent, triggerEvent, fileList) {
+			if (!fileList) {
+				console.warn('FileActions.display() MUST be called with a OCA.Files.FileList instance');
+				return;
+			}
+			this.currentFile = parent;
+			var self = this;
+			var $tr = parent.closest('tr');
+			var actions = this.getActions(
+				this.getCurrentMimeType(),
+				this.getCurrentType(),
+				this.getCurrentPermissions()
+			);
+			var nameLinks;
+			if ($tr.data('renaming')) {
+				return;
+			}
+
+			// recreate fileactions container
+			nameLinks = parent.children('a.name');
+			nameLinks.find('.fileactions, .nametext .action').remove();
+			nameLinks.append('<span class="fileactions" />');
+			var defaultAction = this.getDefaultFileAction(
+				this.getCurrentMimeType(),
+				this.getCurrentType(),
+				this.getCurrentPermissions()
+			);
+
+			var context = {
+				$file: $tr,
+				fileActions: this,
+				fileList: fileList
+			};
+
+			$.each(actions, function (name, actionSpec) {
+				if (actionSpec.type === FileActions.TYPE_INLINE) {
+					self._renderInlineAction(
+						actionSpec,
+						defaultAction && actionSpec.name === defaultAction.name,
+						context
+					);
+				}
+			});
+
+			function objectValues(obj) {
+				var res = [];
+				for (var i in obj) {
+					if (obj.hasOwnProperty(i)) {
+						res.push(obj[i]);
+					}
+				}
+				return res;
+			}
+			// polyfill
+			if (!Object.values) {
+				Object.values = objectValues;
+			}
+
+			var menuActions = Object.values(this.actions.all).filter(function (action) {
+				return action.type !== OCA.Files.FileActions.TYPE_INLINE;
+			});
+			// do not render the menu if nothing is in it
+			if (menuActions.length > 0) {
+				this._renderMenuTrigger($tr, context);
+			}
+
+			if (triggerEvent){
+				fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr}));
+			}
+		},
+		getCurrentFile: function () {
+			return this.currentFile.parent().attr('data-file');
+		},
+		getCurrentMimeType: function () {
+			return this.currentFile.parent().attr('data-mime');
+		},
+		getCurrentType: function () {
+			return this.currentFile.parent().attr('data-type');
+		},
+		getCurrentPermissions: function () {
+			return this.currentFile.parent().data('permissions');
+		},
+
+		/**
+		 * Register the actions that are used by default for the files app.
+		 */
+		registerDefaultActions: function() {
+			this.registerAction({
+				name: 'Download',
+				displayName: t('files', 'Download'),
+				order: -20,
+				mime: 'all',
+				permissions: OC.PERMISSION_READ,
+				iconClass: 'icon-download',
+				actionHandler: function (filename, context) {
+					var dir = context.dir || context.fileList.getCurrentDirectory();
+					var isDir = context.$file.attr('data-type') === 'dir';
+					var url = context.fileList.getDownloadUrl(filename, dir, isDir);
+
+					var downloadFileaction = $(context.$file).find('.fileactions .action-download');
+
+					// don't allow a second click on the download action
+					if(downloadFileaction.hasClass('disabled')) {
+						return;
+					}
+
+					if (url) {
+						var disableLoadingState = function() {
+							context.fileList.showFileBusyState(filename, false);
+						};
+
+						context.fileList.showFileBusyState(filename, true);
+						OCA.Files.Files.handleDownload(url, disableLoadingState);
+					}
+				}
+			});
+
+			this.registerAction({
+				name: 'Rename',
+				displayName: t('files', 'Rename'),
+				mime: 'all',
+				order: -30,
+				permissions: OC.PERMISSION_UPDATE,
+				iconClass: 'icon-rename',
+				actionHandler: function (filename, context) {
+					context.fileList.rename(filename);
+				}
+			});
+
+			this.registerAction({
+				name: 'MoveCopy',
+				displayName: function(context) {
+					var permissions = context.fileInfoModel.attributes.permissions;
+					if (permissions & OC.PERMISSION_UPDATE) {
+						return t('files', 'Move or copy');
+					}
+					return t('files', 'Copy');
+				},
+				mime: 'all',
+				order: -25,
+				permissions: $('#isPublic').val() ? OC.PERMISSION_UPDATE : OC.PERMISSION_READ,
+				iconClass: 'icon-external',
+				actionHandler: function (filename, context) {
+					var permissions = context.fileInfoModel.attributes.permissions;
+					var actions = OC.dialogs.FILEPICKER_TYPE_COPY;
+					if (permissions & OC.PERMISSION_UPDATE) {
+						actions = OC.dialogs.FILEPICKER_TYPE_COPY_MOVE;
+					}
+					var dialogDir = context.dir;
+					if (typeof context.fileList.dirInfo.dirLastCopiedTo !== 'undefined') {
+						dialogDir = context.fileList.dirInfo.dirLastCopiedTo;
+					}
+					OC.dialogs.filepicker(t('files', 'Choose target folder'), function(targetPath, type) {
+						if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
+							context.fileList.copy(filename, targetPath, false, context.dir);
+						}
+						if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
+							context.fileList.move(filename, targetPath, false, context.dir);
+						}
+						context.fileList.dirInfo.dirLastCopiedTo = targetPath; 
+					}, false, "httpd/unix-directory", true, actions, dialogDir);
+				}
+			});
+
+			this.registerAction({
+				name: 'Open',
+				mime: 'dir',
+				permissions: OC.PERMISSION_READ,
+				icon: '',
+				actionHandler: function (filename, context) {
+					var dir = context.$file.attr('data-path') || context.fileList.getCurrentDirectory();
+					if (OCA.Files.App && OCA.Files.App.getActiveView() !== 'files') {
+						OCA.Files.App.setActiveView('files', {silent: true});
+						OCA.Files.App.fileList.changeDirectory(OC.joinPaths(dir, filename), true, true);
+					} else {
+						context.fileList.changeDirectory(OC.joinPaths(dir, filename), true, false, parseInt(context.$file.attr('data-id'), 10));
+					}
+				},
+				displayName: t('files', 'Open')
+			});
+
+			this.registerAction({
+				name: 'Delete',
+				displayName: function(context) {
+					var mountType = context.$file.attr('data-mounttype');
+					var type = context.$file.attr('data-type');
+					var deleteTitle = (type && type === 'file')
+						? t('files', 'Delete file')
+						: t('files', 'Delete folder')
+					if (mountType === 'external-root') {
+						deleteTitle = t('files', 'Disconnect storage');
+					} else if (mountType === 'shared-root') {
+						deleteTitle = t('files', 'Unshare');
+					}
+					return deleteTitle;
+				},
+				mime: 'all',
+				order: 1000,
+				// permission is READ because we show a hint instead if there is no permission
+				permissions: OC.PERMISSION_DELETE,
+				iconClass: 'icon-delete',
+				actionHandler: function(fileName, context) {
+					// if there is no permission to delete do nothing
+					if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) {
+						return;
+					}
+					context.fileList.do_delete(fileName, context.dir);
+					$('.tipsy').remove();
+				}
+			});
+
+			this.setDefault('dir', 'Open');
+		}
+	};
+
+	OCA.Files.FileActions = FileActions;
+
+	/**
+	 * Replaces the button icon with a loading spinner and vice versa
+	 * - also adds the class disabled to the passed in element
+	 *
+	 * @param {jQuery} $buttonElement The button element
+	 * @param {boolean} showIt whether to show the spinner(true) or to hide it(false)
+	 */
+	OCA.Files.FileActions.updateFileActionSpinner = function($buttonElement, showIt) {
+		var $icon = $buttonElement.find('.icon');
+		if (showIt) {
+			var $loadingIcon = $('<span class="icon icon-loading-small"></span>');
+			$icon.after($loadingIcon);
+			$icon.addClass('hidden');
+		} else {
+			$buttonElement.find('.icon-loading-small').remove();
+			$buttonElement.find('.icon').removeClass('hidden');
+		}
+	};
+
+	/**
+	 * File action attributes.
+	 *
+	 * @todo make this a real class in the future
+	 * @typedef {Object} OCA.Files.FileAction
+	 *
+	 * @property {String} name identifier of the action
+	 * @property {(String|OCA.Files.FileActions~displayNameFunction)} displayName
+	 * display name string for the action, or function that returns the display name.
+	 * Defaults to the name given in name property
+	 * @property {String} mime mime type
+	 * @property {int} permissions permissions
+	 * @property {(Function|String)} icon icon path to the icon or function that returns it (deprecated, use iconClass instead)
+	 * @property {(String|OCA.Files.FileActions~iconClassFunction)} iconClass class name of the icon (recommended for theming)
+	 * @property {OCA.Files.FileActions~renderActionFunction} [render] optional rendering function
+	 * @property {OCA.Files.FileActions~actionHandler} actionHandler action handler function
+	 */
+
+	/**
+	 * File action context attributes.
+	 *
+	 * @typedef {Object} OCA.Files.FileActionContext
+	 *
+	 * @property {Object} $file jQuery file row element
+	 * @property {OCA.Files.FileActions} fileActions file actions object
+	 * @property {OCA.Files.FileList} fileList file list object
+	 */
+
+	/**
+	 * Render function for actions.
+	 * The function must render a link element somewhere in the DOM
+	 * and return it. The function should NOT register the event handler
+	 * as this will be done after the link was returned.
+	 *
+	 * @callback OCA.Files.FileActions~renderActionFunction
+	 * @param {OCA.Files.FileAction} actionSpec action definition
+	 * @param {Object} $row row container
+	 * @param {boolean} isDefault true if the action is the default one,
+	 * false otherwise
+	 * @return {Object} jQuery link object
+	 */
+
+	/**
+	 * Display name function for actions.
+	 * The function returns the display name of the action using
+	 * the given context information..
+	 *
+	 * @callback OCA.Files.FileActions~displayNameFunction
+	 * @param {OCA.Files.FileActionContext} context action context
+	 * @return {String} display name
+	 */
+
+	/**
+	 * Icon class function for actions.
+	 * The function returns the icon class of the action using
+	 * the given context information.
+	 *
+	 * @callback OCA.Files.FileActions~iconClassFunction
+	 * @param {String} fileName name of the file on which the action must be performed
+	 * @param {OCA.Files.FileActionContext} context action context
+	 * @return {String} icon class
+	 */
+
+	/**
+	 * Action handler function for file actions
+	 *
+	 * @callback OCA.Files.FileActions~actionHandler
+	 * @param {String} fileName name of the file on which the action must be performed
+	 * @param context context
+	 * @param {String} context.dir directory of the file
+	 * @param {OCA.Files.FileInfoModel} fileInfoModel file info model
+	 * @param {Object} [context.$file] jQuery element of the file [DEPRECATED]
+	 * @param {OCA.Files.FileList} [context.fileList] the FileList instance on which the action occurred [DEPRECATED]
+	 * @param {OCA.Files.FileActions} context.fileActions the FileActions instance on which the action occurred
+	 */
+
+	// global file actions to be used by all lists
+	OCA.Files.fileActions = new OCA.Files.FileActions();
+	OCA.Files.legacyFileActions = new OCA.Files.FileActions();
+
+	// for backward compatibility
+	//
+	// legacy apps are expecting a stateful global FileActions object to register
+	// their actions on. Since legacy apps are very likely to break with other
+	// FileList views than the main one ("All files"), actions registered
+	// through window.FileActions will be limited to the main file list.
+	// @deprecated use OCA.Files.FileActions instead
+	window.FileActions = OCA.Files.legacyFileActions;
+	window.FileActions.register = function (mime, name, permissions, icon, action, displayName) {
+		console.warn('FileActions.register() is deprecated, please use OCA.Files.fileActions.register() instead', arguments);
+		OCA.Files.FileActions.prototype.register.call(
+				window.FileActions, mime, name, permissions, icon, action, displayName
+		);
+	};
+	window.FileActions.display = function (parent, triggerEvent, fileList) {
+		fileList = fileList || OCA.Files.App.fileList;
+		console.warn('FileActions.display() is deprecated, please use OCA.Files.fileActions.register() which automatically redisplays actions', mime, name);
+		OCA.Files.FileActions.prototype.display.call(window.FileActions, parent, triggerEvent, fileList);
+	};
+})();
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+	/**
+	 * Construct a new FileActionsMenu instance
+	 * @constructs FileActionsMenu
+	 * @memberof OCA.Files
+	 */
+	var FileActionsMenu = OC.Backbone.View.extend({
+		tagName: 'div',
+		className: 'fileActionsMenu popovermenu bubble hidden open menu',
+
+		/**
+		 * Current context
+		 *
+		 * @type OCA.Files.FileActionContext
+		 */
+		_context: null,
+
+		events: {
+			'click a.action': '_onClickAction'
+		},
+
+		template: function(data) {
+			return OCA.Files.Templates['fileactionsmenu'](data);
+		},
+
+		/**
+		 * Event handler whenever an action has been clicked within the menu
+		 *
+		 * @param {Object} event event object
+		 */
+		_onClickAction: function(event) {
+			var $target = $(event.target);
+			if (!$target.is('a')) {
+				$target = $target.closest('a');
+			}
+			var fileActions = this._context.fileActions;
+			var actionName = $target.attr('data-action');
+			var actions = fileActions.getActions(
+				fileActions.getCurrentMimeType(),
+				fileActions.getCurrentType(),
+				fileActions.getCurrentPermissions()
+			);
+			var actionSpec = actions[actionName];
+			var fileName = this._context.$file.attr('data-file');
+
+			event.stopPropagation();
+			event.preventDefault();
+
+			OC.hideMenus();
+
+			actionSpec.action(
+				fileName,
+				this._context
+			);
+		},
+
+		/**
+		 * Renders the menu with the currently set items
+		 */
+		render: function() {
+			var self = this;
+			var fileActions = this._context.fileActions;
+			var actions = fileActions.getActions(
+				fileActions.getCurrentMimeType(),
+				fileActions.getCurrentType(),
+				fileActions.getCurrentPermissions()
+			);
+
+			var defaultAction = fileActions.getDefaultFileAction(
+				fileActions.getCurrentMimeType(),
+				fileActions.getCurrentType(),
+				fileActions.getCurrentPermissions()
+			);
+
+			var items = _.filter(actions, function(actionSpec) {
+				return !defaultAction || actionSpec.name !== defaultAction.name;
+			});
+			items = _.map(items, function(item) {
+				if (_.isFunction(item.displayName)) {
+					item = _.extend({}, item);
+					item.displayName = item.displayName(self._context);
+				}
+				if (_.isFunction(item.iconClass)) {
+					var fileName = self._context.$file.attr('data-file');
+					item = _.extend({}, item);
+					item.iconClass = item.iconClass(fileName, self._context);
+				}
+				if (_.isFunction(item.icon)) {
+					var fileName = self._context.$file.attr('data-file');
+					item = _.extend({}, item);
+					item.icon = item.icon(fileName, self._context);
+				}
+				item.inline = item.type === OCA.Files.FileActions.TYPE_INLINE
+				return item;
+			});
+			items = items.sort(function(actionA, actionB) {
+				var orderA = actionA.order || 0;
+				var orderB = actionB.order || 0;
+				if (orderB === orderA) {
+					return OC.Util.naturalSortCompare(actionA.displayName, actionB.displayName);
+				}
+				return orderA - orderB;
+			});
+
+			items = _.map(items, function(item) {
+				item.nameLowerCase = item.name.toLowerCase();
+				return item;
+			});
+
+			this.$el.html(this.template({
+				items: items
+			}));
+		},
+
+		/**
+		 * Displays the menu under the given element
+		 *
+		 * @param {OCA.Files.FileActionContext} context context
+		 * @param {Object} $trigger trigger element
+		 */
+		show: function(context) {
+			this._context = context;
+
+			this.render();
+			this.$el.removeClass('hidden');
+
+			OC.showMenu(null, this.$el);
+		}
+	});
+
+	OCA.Files.FileActionsMenu = FileActionsMenu;
+
+})();
+
+
+
+/*
+ * Copyright (c) 2014
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global getURLParameter */
+/**
+ * Utility class for file related operations
+ */
+(function() {
+	var Files = {
+		// file space size sync
+		_updateStorageStatistics: function(currentDir) {
+			var state = Files.updateStorageStatistics;
+			if (state.dir){
+				if (state.dir === currentDir) {
+					return;
+				}
+				// cancel previous call, as it was for another dir
+				state.call.abort();
+			}
+			state.dir = currentDir;
+			state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php') + '?dir=' + encodeURIComponent(currentDir),function(response) {
+				state.dir = null;
+				state.call = null;
+				Files.updateMaxUploadFilesize(response);
+			});
+		},
+		// update quota
+		updateStorageQuotas: function() {
+			Files._updateStorageQuotasThrottled();
+		},
+		_updateStorageQuotas: function() {
+			var state = Files.updateStorageQuotas;
+			state.call = $.getJSON(OC.filePath('files','ajax','getstoragestats.php'),function(response) {
+				Files.updateQuota(response);
+			});
+		},
+		/**
+		 * Update storage statistics such as free space, max upload,
+		 * etc based on the given directory.
+		 *
+		 * Note this function is debounced to avoid making too
+		 * many ajax calls in a row.
+		 *
+		 * @param dir directory
+		 * @param force whether to force retrieving
+		 */
+		updateStorageStatistics: function(dir, force) {
+			if (!OC.currentUser) {
+				return;
+			}
+
+			if (force) {
+				Files._updateStorageStatistics(dir);
+			}
+			else {
+				Files._updateStorageStatisticsDebounced(dir);
+			}
+		},
+
+		updateMaxUploadFilesize:function(response) {
+			if (response === undefined) {
+				return;
+			}
+			if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) {
+				$('#free_space').val(response.data.freeSpace);
+				$('#upload.button').attr('data-original-title', response.data.maxHumanFilesize);
+				$('#usedSpacePercent').val(response.data.usedSpacePercent);
+				$('#owner').val(response.data.owner);
+				$('#ownerDisplayName').val(response.data.ownerDisplayName);
+				Files.displayStorageWarnings();
+				OCA.Files.App.fileList._updateDirectoryPermissions();
+			}
+			if (response[0] === undefined) {
+				return;
+			}
+			if (response[0].uploadMaxFilesize !== undefined) {
+				$('#upload.button').attr('data-original-title', response[0].maxHumanFilesize);
+				$('#usedSpacePercent').val(response[0].usedSpacePercent);
+				Files.displayStorageWarnings();
+			}
+
+		},
+
+		updateQuota:function(response) {
+			if (response === undefined) {
+				return;
+			}
+			if (response.data !== undefined
+			 && response.data.quota !== undefined
+			 && response.data.used !== undefined
+			 && response.data.usedSpacePercent !== undefined) {
+				var humanUsed = OC.Util.humanFileSize(response.data.used, true);
+				var humanQuota = OC.Util.humanFileSize(response.data.quota, true);
+				if (response.data.quota > 0) {
+					$('#quota').attr('data-original-title', Math.floor(response.data.used/response.data.quota*1000)/10 + '%');
+					$('#quota progress').val(response.data.usedSpacePercent);
+					$('#quotatext').text(t('files', '{used} of {quota} used', {used: humanUsed, quota: humanQuota}));
+				} else {
+					$('#quotatext').text(t('files', '{used} used', {used: humanUsed}));
+				}
+				if (response.data.usedSpacePercent > 80) {
+					$('#quota progress').addClass('warn');
+				} else {
+					$('#quota progress').removeClass('warn');
+				}
+			}
+
+		},
+
+		/**
+		 * Fix path name by removing double slash at the beginning, if any
+		 */
+		fixPath: function(fileName) {
+			if (fileName.substr(0, 2) == '//') {
+				return fileName.substr(1);
+			}
+			return fileName;
+		},
+
+		/**
+		 * Checks whether the given file name is valid.
+		 * @param name file name to check
+		 * @return true if the file name is valid.
+		 * Throws a string exception with an error message if
+		 * the file name is not valid
+		 */
+		isFileNameValid: function (name) {
+			var trimmedName = name.trim();
+			if (trimmedName === '.' || trimmedName === '..')
+			{
+				throw t('files', '"{name}" is an invalid file name.', {name: name});
+			} else if (trimmedName.length === 0) {
+				throw t('files', 'File name cannot be empty.');
+			} else if (trimmedName.indexOf('/') !== -1) {
+				throw t('files', '"/" is not allowed inside a file name.');
+			} else if (!!(trimmedName.match(OC.config.blacklist_files_regex))) {
+				throw t('files', '"{name}" is not an allowed filetype', {name: name});
+			}
+
+			return true;
+		},
+		displayStorageWarnings: function() {
+			if (!OC.Notification.isHidden()) {
+				return;
+			}
+
+			var usedSpacePercent = $('#usedSpacePercent').val(),
+				owner = $('#owner').val(),
+				ownerDisplayName = $('#ownerDisplayName').val();
+			if (usedSpacePercent > 98) {
+				if (owner !== OC.getCurrentUser().uid) {
+					OC.Notification.show(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!',
+						{owner: ownerDisplayName}), {type: 'error'}
+					);
+					return;
+				}
+				OC.Notification.show(t('files',
+					'Your storage is full, files can not be updated or synced anymore!'),
+					{type : 'error'}
+				);
+				return;
+			}
+			if (usedSpacePercent > 90) {
+				if (owner !== OC.getCurrentUser().uid) {
+					OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)',
+						{
+							usedSpacePercent: usedSpacePercent,
+							owner: ownerDisplayName
+						}),
+						{
+							type: 'error'
+						}
+					);
+					return;
+				}
+				OC.Notification.show(t('files', 'Your storage is almost full ({usedSpacePercent}%)',
+					{usedSpacePercent: usedSpacePercent}),
+					{type : 'error'}
+				);
+			}
+		},
+
+		/**
+		 * Returns the download URL of the given file(s)
+		 * @param {string} filename string or array of file names to download
+		 * @param {string} [dir] optional directory in which the file name is, defaults to the current directory
+		 * @param {bool} [isDir=false] whether the given filename is a directory and might need a special URL
+		 */
+		getDownloadUrl: function(filename, dir, isDir) {
+			if (!_.isArray(filename) && !isDir) {
+				var pathSections = dir.split('/');
+				pathSections.push(filename);
+				var encodedPath = '';
+				_.each(pathSections, function(section) {
+					if (section !== '') {
+						encodedPath += '/' + encodeURIComponent(section);
+					}
+				});
+				return OC.linkToRemoteBase('webdav') + encodedPath;
+			}
+
+			if (_.isArray(filename)) {
+				filename = JSON.stringify(filename);
+			}
+
+			var params = {
+				dir: dir,
+				files: filename
+			};
+			return this.getAjaxUrl('download', params);
+		},
+
+		/**
+		 * Returns the ajax URL for a given action
+		 * @param action action string
+		 * @param params optional params map
+		 */
+		getAjaxUrl: function(action, params) {
+			var q = '';
+			if (params) {
+				q = '?' + OC.buildQueryString(params);
+			}
+			return OC.filePath('files', 'ajax', action + '.php') + q;
+		},
+
+		/**
+		 * Fetch the icon url for the mimetype
+		 * @param {string} mime The mimetype
+		 * @param {Files~mimeicon} ready Function to call when mimetype is retrieved
+		 * @deprecated use OC.MimeType.getIconUrl(mime)
+		 */
+		getMimeIcon: function(mime, ready) {
+			ready(OC.MimeType.getIconUrl(mime));
+		},
+
+		/**
+		 * Generates a preview URL based on the URL space.
+		 * @param urlSpec attributes for the URL
+		 * @param {int} urlSpec.x width
+		 * @param {int} urlSpec.y height
+		 * @param {String} urlSpec.file path to the file
+		 * @return preview URL
+		 * @deprecated used OCA.Files.FileList.generatePreviewUrl instead
+		 */
+		generatePreviewUrl: function(urlSpec) {
+			console.warn('DEPRECATED: please use generatePreviewUrl() from an OCA.Files.FileList instance');
+			return OCA.Files.App.fileList.generatePreviewUrl(urlSpec);
+		},
+
+		/**
+		 * Lazy load preview
+		 * @deprecated used OCA.Files.FileList.lazyLoadPreview instead
+		 */
+		lazyLoadPreview : function(path, mime, ready, width, height, etag) {
+			console.warn('DEPRECATED: please use lazyLoadPreview() from an OCA.Files.FileList instance');
+			return FileList.lazyLoadPreview({
+				path: path,
+				mime: mime,
+				callback: ready,
+				width: width,
+				height: height,
+				etag: etag
+			});
+		},
+
+		/**
+		 * Initialize the files view
+		 */
+		initialize: function() {
+			Files.bindKeyboardShortcuts(document, $);
+
+			// TODO: move file list related code (upload) to OCA.Files.FileList
+			$('#file_action_panel').attr('activeAction', false);
+
+			// drag&drop support using jquery.fileupload
+			// TODO use OC.dialogs
+			$(document).bind('drop dragover', function (e) {
+					e.preventDefault(); // prevent browser from doing anything, if file isn't dropped in dropZone
+				});
+
+			// display storage warnings
+			setTimeout(Files.displayStorageWarnings, 100);
+
+			// only possible at the moment if user is logged in or the files app is loaded
+			if (OC.currentUser && OCA.Files.App) {
+				// start on load - we ask the server every 5 minutes
+				var func = _.bind(OCA.Files.App.fileList.updateStorageStatistics, OCA.Files.App.fileList);
+				var updateStorageStatisticsInterval = 5*60*1000;
+				var updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
+
+				// TODO: this should also stop when switching to another view
+				// Use jquery-visibility to de-/re-activate file stats sync
+				if ($.support.pageVisibility) {
+					$(document).on({
+						'show': function() {
+							if (!updateStorageStatisticsIntervalId) {
+								updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval);
+							}
+						},
+						'hide': function() {
+							clearInterval(updateStorageStatisticsIntervalId);
+							updateStorageStatisticsIntervalId = 0;
+						}
+					});
+				}
+			}
+
+
+			$('#webdavurl').on('click touchstart', function () {
+				this.focus();
+				this.setSelectionRange(0, this.value.length);
+			});
+
+			$('#upload').tooltip({placement:'right'});
+
+			//FIXME scroll to and highlight preselected file
+			/*
+			if (getURLParameter('scrollto')) {
+				FileList.scrollTo(getURLParameter('scrollto'));
+			}
+			*/
+		},
+
+		/**
+		 * Handles the download and calls the callback function once the download has started
+		 * - browser sends download request and adds parameter with a token
+		 * - server notices this token and adds a set cookie to the download response
+		 * - browser now adds this cookie for the domain
+		 * - JS periodically checks for this cookie and then knows when the download has started to call the callback
+		 *
+		 * @param {string} url download URL
+		 * @param {function} callback function to call once the download has started
+		 */
+		handleDownload: function(url, callback) {
+			var randomToken = Math.random().toString(36).substring(2),
+				checkForDownloadCookie = function() {
+					if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){
+						return false;
+					} else {
+						callback();
+						return true;
+					}
+				};
+
+			if (url.indexOf('?') >= 0) {
+				url += '&';
+			} else {
+				url += '?';
+			}
+			OC.redirect(url + 'downloadStartSecret=' + randomToken);
+			OC.Util.waitFor(checkForDownloadCookie, 500);
+		}
+	};
+
+	Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250);
+	Files._updateStorageQuotasThrottled = _.throttle(Files._updateStorageQuotas, 30000);
+	OCA.Files.Files = Files;
+})();
+
+// TODO: move to FileList
+var createDragShadow = function(event) {
+	// FIXME: inject file list instance somehow
+	/* global FileList, Files */
+
+	//select dragged file
+	var isDragSelected = $(event.target).parents('tr').find('td input:first').prop('checked');
+	if (!isDragSelected) {
+		//select dragged file
+		FileList._selectFileEl($(event.target).parents('tr:first'), true, false);
+	}
+
+	// do not show drag shadow for too many files
+	var selectedFiles = _.first(FileList.getSelectedFiles(), FileList.pageSize());
+	selectedFiles = _.sortBy(selectedFiles, FileList._fileInfoCompare);
+
+	if (!isDragSelected && selectedFiles.length === 1) {
+		//revert the selection
+		FileList._selectFileEl($(event.target).parents('tr:first'), false, false);
+	}
+
+	// build dragshadow
+	var dragshadow = $('<table class="dragshadow"></table>');
+	var tbody = $('<tbody></tbody>');
+	dragshadow.append(tbody);
+
+	var dir = FileList.getCurrentDirectory();
+
+	$(selectedFiles).each(function(i,elem) {
+		// TODO: refactor this with the table row creation code
+		var newtr = $('<tr/>')
+			.attr('data-dir', dir)
+			.attr('data-file', elem.name)
+			.attr('data-origin', elem.origin);
+		newtr.append($('<td class="filename" />').text(elem.name).css('background-size', 32));
+		newtr.append($('<td class="size" />').text(OC.Util.humanFileSize(elem.size)));
+		tbody.append(newtr);
+		if (elem.type === 'dir') {
+			newtr.find('td.filename')
+				.css('background-image', 'url(' + OC.MimeType.getIconUrl('folder') + ')');
+		} else {
+			var path = dir + '/' + elem.name;
+			Files.lazyLoadPreview(path, elem.mimetype, function(previewpath) {
+				newtr.find('td.filename')
+					.css('background-image', 'url(' + previewpath + ')');
+			}, null, null, elem.etag);
+		}
+	});
+
+	return dragshadow;
+};
+
+//options for file drag/drop
+//start&stop handlers needs some cleaning up
+// TODO: move to FileList class
+var dragOptions={
+	revert: 'invalid',
+	revertDuration: 300,
+	opacity: 0.7,
+	appendTo: 'body',
+	cursorAt: { left: 24, top: 18 },
+	helper: createDragShadow,
+	cursor: 'move',
+
+	start: function(event, ui){
+		var $selectedFiles = $('td.filename input:checkbox:checked');
+		if (!$selectedFiles.length) {
+			$selectedFiles = $(this);
+		}
+		$selectedFiles.closest('tr').addClass('animate-opacity dragging');
+		$selectedFiles.closest('tr').filter('.ui-droppable').droppable( 'disable' );
+		// Show breadcrumbs menu
+		$('.crumbmenu').addClass('canDropChildren');
+
+	},
+	stop: function(event, ui) {
+		var $selectedFiles = $('td.filename input:checkbox:checked');
+		if (!$selectedFiles.length) {
+			$selectedFiles = $(this);
+		}
+
+		var $tr = $selectedFiles.closest('tr');
+		$tr.removeClass('dragging');
+		$tr.filter('.ui-droppable').droppable( 'enable' );
+
+		setTimeout(function() {
+			$tr.removeClass('animate-opacity');
+		}, 300);
+		// Hide breadcrumbs menu
+		$('.crumbmenu').removeClass('canDropChildren');
+	},
+	drag: function(event, ui) {
+		var scrollingArea = window;
+		var currentScrollTop = $(scrollingArea).scrollTop();
+		var scrollArea = Math.min(Math.floor($(window).innerHeight() / 2), 100);
+
+		var bottom = $(window).innerHeight() - scrollArea;
+		var top = $(window).scrollTop() + scrollArea;
+		if (event.pageY < top) {
+			$(scrollingArea).animate({
+				scrollTop: currentScrollTop - 10
+			}, 400);
+
+		} else if (event.pageY > bottom) {
+			$(scrollingArea).animate({
+				scrollTop: currentScrollTop + 10
+			}, 400);
+		}
+
+	}
+};
+// sane browsers support using the distance option
+if ( $('html.ie').length === 0) {
+	dragOptions['distance'] = 20;
+}
+
+// TODO: move to FileList class
+var folderDropOptions = {
+	hoverClass: "canDrop",
+	drop: function( event, ui ) {
+		// don't allow moving a file into a selected folder
+		/* global FileList */
+		if ($(event.target).parents('tr').find('td input:first').prop('checked') === true) {
+			return false;
+		}
+
+		var $tr = $(this).closest('tr');
+		if (($tr.data('permissions') & OC.PERMISSION_CREATE) === 0) {
+			FileList._showPermissionDeniedNotification();
+			return false;
+		}
+		var targetPath = FileList.getCurrentDirectory() + '/' + $tr.data('file');
+
+		var files = FileList.getSelectedFiles();
+		if (files.length === 0) {
+			// single one selected without checkbox?
+			files = _.map(ui.helper.find('tr'), function(el) {
+				return FileList.elementToFile($(el));
+			});
+		}
+
+		FileList.move(_.pluck(files, 'name'), targetPath);
+	},
+	tolerance: 'pointer'
+};
+
+// for backward compatibility
+window.Files = OCA.Files.Files;
+
+
+/**
+ * Copyright (c) 2012 Erik Sargent <esthepiking at gmail dot com>
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ */
+/*****************************
+ * Keyboard shortcuts for Files app
+ * ctrl/cmd+n: new folder
+ * ctrl/cmd+shift+n: new file
+ * esc (while new file context menu is open): close menu
+ * up/down: select file/folder
+ * enter: open file/folder
+ * delete/backspace: delete file/folder
+ *****************************/
+(function(Files) {
+	var keys = [];
+	var keyCodes = {
+		shift: 16,
+		n: 78,
+		cmdFirefox: 224,
+		cmdOpera: 17,
+		leftCmdWebKit: 91,
+		rightCmdWebKit: 93,
+		ctrl: 17,
+		esc: 27,
+		downArrow: 40,
+		upArrow: 38,
+		enter: 13,
+		del: 46
+	};
+
+	function removeA(arr) {
+		var what, a = arguments,
+			L = a.length,
+			ax;
+		while (L > 1 && arr.length) {
+			what = a[--L];
+			while ((ax = arr.indexOf(what)) !== -1) {
+				arr.splice(ax, 1);
+			}
+		}
+		return arr;
+	}
+
+	function newFile() {
+		$("#new").addClass("active");
+		$(".popup.popupTop").toggle(true);
+		$('#new li[data-type="file"]').trigger('click');
+		removeA(keys, keyCodes.n);
+	}
+
+	function newFolder() {
+		$("#new").addClass("active");
+		$(".popup.popupTop").toggle(true);
+		$('#new li[data-type="folder"]').trigger('click');
+		removeA(keys, keyCodes.n);
+	}
+
+	function esc() {
+		$("#controls").trigger('click');
+	}
+
+	function down() {
+		var select = -1;
+		$("#fileList tr").each(function(index) {
+			if ($(this).hasClass("mouseOver")) {
+				select = index + 1;
+				$(this).removeClass("mouseOver");
+			}
+		});
+		if (select === -1) {
+			$("#fileList tr:first").addClass("mouseOver");
+		} else {
+			$("#fileList tr").each(function(index) {
+				if (index === select) {
+					$(this).addClass("mouseOver");
+				}
+			});
+		}
+	}
+
+	function up() {
+		var select = -1;
+		$("#fileList tr").each(function(index) {
+			if ($(this).hasClass("mouseOver")) {
+				select = index - 1;
+				$(this).removeClass("mouseOver");
+			}
+		});
+		if (select === -1) {
+			$("#fileList tr:last").addClass("mouseOver");
+		} else {
+			$("#fileList tr").each(function(index) {
+				if (index === select) {
+					$(this).addClass("mouseOver");
+				}
+			});
+		}
+	}
+
+	function enter() {
+		$("#fileList tr").each(function(index) {
+			if ($(this).hasClass("mouseOver")) {
+				$(this).removeClass("mouseOver");
+				$(this).find("span.nametext").trigger('click');
+			}
+		});
+	}
+
+	function del() {
+		$("#fileList tr").each(function(index) {
+			if ($(this).hasClass("mouseOver")) {
+				$(this).removeClass("mouseOver");
+				$(this).find("a.action.delete").trigger('click');
+			}
+		});
+	}
+
+	function rename() {
+		$("#fileList tr").each(function(index) {
+			if ($(this).hasClass("mouseOver")) {
+				$(this).removeClass("mouseOver");
+				$(this).find("a[data-action='Rename']").trigger('click');
+			}
+		});
+	}
+	Files.bindKeyboardShortcuts = function(document, $) {
+		$(document).keydown(function(event) { //check for modifier keys
+            if(!$(event.target).is('body')) {
+                return;
+            }
+			var preventDefault = false;
+			if ($.inArray(event.keyCode, keys) === -1) {
+				keys.push(event.keyCode);
+			}
+			if (
+			$.inArray(keyCodes.n, keys) !== -1 && ($.inArray(keyCodes.cmdFirefox, keys) !== -1 || $.inArray(keyCodes.cmdOpera, keys) !== -1 || $.inArray(keyCodes.leftCmdWebKit, keys) !== -1 || $.inArray(keyCodes.rightCmdWebKit, keys) !== -1 || $.inArray(keyCodes.ctrl, keys) !== -1 || event.ctrlKey)) {
+				preventDefault = true; //new file/folder prevent browser from responding
+			}
+			if (preventDefault) {
+				event.preventDefault(); //Prevent web browser from responding
+				event.stopPropagation();
+				return false;
+			}
+		});
+		$(document).keyup(function(event) {
+			// do your event.keyCode checks in here
+			if (
+			$.inArray(keyCodes.n, keys) !== -1 && ($.inArray(keyCodes.cmdFirefox, keys) !== -1 || $.inArray(keyCodes.cmdOpera, keys) !== -1 || $.inArray(keyCodes.leftCmdWebKit, keys) !== -1 || $.inArray(keyCodes.rightCmdWebKit, keys) !== -1 || $.inArray(keyCodes.ctrl, keys) !== -1 || event.ctrlKey)) {
+				if ($.inArray(keyCodes.shift, keys) !== -1) { //16=shift, New File
+					newFile();
+				} else { //New Folder
+					newFolder();
+				}
+			} else if ($("#new").hasClass("active") && $.inArray(keyCodes.esc, keys) !== -1) { //close new window
+				esc();
+			} else if ($.inArray(keyCodes.downArrow, keys) !== -1) { //select file
+				down();
+			} else if ($.inArray(keyCodes.upArrow, keys) !== -1) { //select file
+				up();
+			} else if (!$("#new").hasClass("active") && $.inArray(keyCodes.enter, keys) !== -1) { //open file
+				enter();
+			} else if (!$("#new").hasClass("active") && $.inArray(keyCodes.del, keys) !== -1) { //delete file
+				del();
+			}
+			removeA(keys, event.keyCode);
+		});
+	};
+})((OCA.Files && OCA.Files.Files) || {});
+
+
+/*
+ * @Copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * @author Vincent Petry
+ * @author Felix Nüsse <felix.nuesse@t-online.de>
+ *
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function () {
+
+	/**
+	 * @class OCA.Files.Navigation
+	 * @classdesc Navigation control for the files app sidebar.
+	 *
+	 * @param $el element containing the navigation
+	 */
+	var Navigation = function ($el) {
+		this.initialize($el);
+	};
+
+	/**
+	 * @memberof OCA.Files
+	 */
+	Navigation.prototype = {
+
+		/**
+		 * Currently selected item in the list
+		 */
+		_activeItem: null,
+
+		/**
+		 * Currently selected container
+		 */
+		$currentContent: null,
+
+		/**
+		 * Key for the quick-acces-list
+		 */
+		$quickAccessListKey: 'sublist-favorites',
+		/**
+		 * Initializes the navigation from the given container
+		 *
+		 * @private
+		 * @param $el element containing the navigation
+		 */
+		initialize: function ($el) {
+			this.$el = $el;
+			this._activeItem = null;
+			this.$currentContent = null;
+			this._setupEvents();
+
+			this.setInitialQuickaccessSettings();
+		},
+
+		/**
+		 * Setup UI events
+		 */
+		_setupEvents: function () {
+			this.$el.on('click', 'li a', _.bind(this._onClickItem, this));
+			this.$el.on('click', 'li button', _.bind(this._onClickMenuButton, this));
+
+			var trashBinElement = $('.nav-trashbin');
+			trashBinElement.droppable({
+				over: function (event, ui) {
+					trashBinElement.addClass('dropzone-background');
+				},
+				out: function (event, ui) {
+					trashBinElement.removeClass('dropzone-background');
+				},
+				activate: function (event, ui) {
+					var element = trashBinElement.find('a').first();
+					element.addClass('nav-icon-trashbin-starred').removeClass('nav-icon-trashbin');
+				},
+				deactivate: function (event, ui) {
+					var element = trashBinElement.find('a').first();
+					element.addClass('nav-icon-trashbin').removeClass('nav-icon-trashbin-starred');
+				},
+				drop: function (event, ui) {
+					trashBinElement.removeClass('dropzone-background');
+
+					var $selectedFiles = $(ui.draggable);
+
+					// FIXME: when there are a lot of selected files the helper
+					// contains only a subset of them; the list of selected
+					// files should be gotten from the file list instead to
+					// ensure that all of them are removed.
+					var item = ui.helper.find('tr');
+					for (var i = 0; i < item.length; i++) {
+						$selectedFiles.trigger('droppedOnTrash', item[i].getAttribute('data-file'), item[i].getAttribute('data-dir'));
+					}
+				}
+			});
+		},
+
+		/**
+		 * Returns the container of the currently active app.
+		 *
+		 * @return app container
+		 */
+		getActiveContainer: function () {
+			return this.$currentContent;
+		},
+
+		/**
+		 * Returns the currently active item
+		 *
+		 * @return item ID
+		 */
+		getActiveItem: function () {
+			return this._activeItem;
+		},
+
+		/**
+		 * Switch the currently selected item, mark it as selected and
+		 * make the content container visible, if any.
+		 *
+		 * @param string itemId id of the navigation item to select
+		 * @param array options "silent" to not trigger event
+		 */
+		setActiveItem: function (itemId, options) {
+			var currentItem = this.$el.find('li[data-id="' + itemId + '"]');
+			var itemDir = currentItem.data('dir');
+			var itemView = currentItem.data('view');
+			var oldItemId = this._activeItem;
+			if (itemId === this._activeItem) {
+				if (!options || !options.silent) {
+					this.$el.trigger(
+						new $.Event('itemChanged', {
+							itemId: itemId,
+							previousItemId: oldItemId,
+							dir: itemDir,
+							view: itemView
+						})
+					);
+				}
+				return;
+			}
+			this.$el.find('li a').removeClass('active');
+			if (this.$currentContent) {
+				this.$currentContent.addClass('hidden');
+				this.$currentContent.trigger(jQuery.Event('hide'));
+			}
+			this._activeItem = itemId;
+			currentItem.children('a').addClass('active');
+			this.$currentContent = $('#app-content-' + (typeof itemView === 'string' && itemView !== '' ? itemView : itemId));
+			this.$currentContent.removeClass('hidden');
+			if (!options || !options.silent) {
+				this.$currentContent.trigger(jQuery.Event('show', {
+					itemId: itemId,
+					previousItemId: oldItemId,
+					dir: itemDir,
+					view: itemView
+				}));
+				this.$el.trigger(
+					new $.Event('itemChanged', {
+						itemId: itemId,
+						previousItemId: oldItemId,
+						dir: itemDir,
+						view: itemView
+					})
+				);
+			}
+		},
+
+		/**
+		 * Returns whether a given item exists
+		 */
+		itemExists: function (itemId) {
+			return this.$el.find('li[data-id="' + itemId + '"]').length;
+		},
+
+		/**
+		 * Event handler for when clicking on an item.
+		 */
+		_onClickItem: function (ev) {
+			var $target = $(ev.target);
+			var itemId = $target.closest('li').attr('data-id');
+			if (!_.isUndefined(itemId)) {
+				this.setActiveItem(itemId);
+			}
+			ev.preventDefault();
+		},
+
+		/**
+		 * Event handler for clicking a button
+		 */
+		_onClickMenuButton: function (ev) {
+			var $target = $(ev.target);
+			var $menu = $target.parent('li');
+			var itemId = $target.closest('button').attr('id');
+
+			var collapsibleToggles = [];
+			var dotmenuToggles = [];
+
+			if ($menu.hasClass('collapsible') && $menu.data('expandedstate')) {
+				$menu.toggleClass('open');
+				var show = $menu.hasClass('open') ? 1 : 0;
+				var key = $menu.data('expandedstate');
+				$.post(OC.generateUrl("/apps/files/api/v1/toggleShowFolder/" + key), {show: show});
+			}
+
+			dotmenuToggles.forEach(function foundToggle (item) {
+				if (item[0] === ("#" + itemId)) {
+					document.getElementById(item[1]).classList.toggle('open');
+				}
+			});
+
+			ev.preventDefault();
+		},
+
+		/**
+		 * Sort initially as setup of sidebar for QuickAccess
+		 */
+		setInitialQuickaccessSettings: function () {
+			var quickAccessKey = this.$quickAccessListKey;
+			var quickAccessMenu = document.getElementById(quickAccessKey);
+			if (quickAccessMenu) {
+				var list = quickAccessMenu.getElementsByTagName('li');
+				this.QuickSort(list, 0, list.length - 1);
+			}
+
+			var favoritesListElement = $(quickAccessMenu).parent();
+			favoritesListElement.droppable({
+				over: function (event, ui) {
+					favoritesListElement.addClass('dropzone-background');
+				},
+				out: function (event, ui) {
+					favoritesListElement.removeClass('dropzone-background');
+				},
+				activate: function (event, ui) {
+					var element = favoritesListElement.find('a').first();
+					element.addClass('nav-icon-favorites-starred').removeClass('nav-icon-favorites');
+				},
+				deactivate: function (event, ui) {
+					var element = favoritesListElement.find('a').first();
+					element.addClass('nav-icon-favorites').removeClass('nav-icon-favorites-starred');
+				},
+				drop: function (event, ui) {
+					favoritesListElement.removeClass('dropzone-background');
+
+					var $selectedFiles = $(ui.draggable);
+
+					if (ui.helper.find('tr').size() === 1) {
+						var $tr = $selectedFiles.closest('tr');
+						if ($tr.attr("data-favorite")) {
+							return;
+						}
+						$selectedFiles.trigger('droppedOnFavorites', $tr.attr('data-file'));
+					} else {
+						// FIXME: besides the issue described for dropping on
+						// the trash bin, for favoriting it is not possible to
+						// use the data from the helper; due to some bugs the
+						// tags are not always added to the selected files, and
+						// thus that data can not be accessed through the helper
+						// to prevent triggering the favorite action on an
+						// already favorited file (which would remove it from
+						// favorites).
+						OC.Notification.showTemporary(t('files', 'You can only favorite a single file or folder at a time'));
+					}
+				}
+			});
+		},
+
+		/**
+		 * Sorting-Algorithm for QuickAccess
+		 */
+		QuickSort: function (list, start, end) {
+			var lastMatch;
+			if (list.length > 1) {
+				lastMatch = this.quicksort_helper(list, start, end);
+				if (start < lastMatch - 1) {
+					this.QuickSort(list, start, lastMatch - 1);
+				}
+				if (lastMatch < end) {
+					this.QuickSort(list, lastMatch, end);
+				}
+			}
+		},
+
+		/**
+		 * Sorting-Algorithm-Helper for QuickAccess
+		 */
+		quicksort_helper: function (list, start, end) {
+			var pivot = Math.floor((end + start) / 2);
+			var pivotElement = this.getCompareValue(list, pivot);
+			var i = start;
+			var j = end;
+
+			while (i <= j) {
+				while (this.getCompareValue(list, i) < pivotElement) {
+					i++;
+				}
+				while (this.getCompareValue(list, j) > pivotElement) {
+					j--;
+				}
+				if (i <= j) {
+					this.swap(list, i, j);
+					i++;
+					j--;
+				}
+			}
+			return i;
+		},
+
+		/**
+		 * Sorting-Algorithm-Helper for QuickAccess
+		 * This method allows easy access to the element which is sorted by.
+		 */
+		getCompareValue: function (nodes, int, strategy) {
+				return nodes[int].getElementsByTagName('a')[0].innerHTML.toLowerCase();
+		},
+
+		/**
+		 * Sorting-Algorithm-Helper for QuickAccess
+		 * This method allows easy swapping of elements.
+		 */
+		swap: function (list, j, i) {
+			var before = function(node, insertNode) {
+				node.parentNode.insertBefore(insertNode, node);
+			}
+			before(list[i], list[j]);
+			before(list[j], list[i]);
+		}
+
+	};
+
+	OCA.Files.Navigation = Navigation;
+
+})();
+
+
+
+
+
+
+

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js.deps


BIN
compose/karmen/data/appdata_occakkp6o8zz/js/files/merged-index.js.gzip


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js.deps

@@ -0,0 +1 @@
+{"\/var\/www\/html\/apps\/gallery\/js\/scripts-for-file-app.json":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/vendor\/bigshot\/bigshot-compressed.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/vendor\/dompurify\/src\/purify.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/galleryutility.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/galleryfileaction.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/slideshow.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/slideshowcontrols.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/slideshowzoomablepreview.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/gallerybutton.js":1570177632,"\/var\/www\/html\/apps\/gallery\/js\/vendor\/nextcloud\/share.js":1570177632}

BIN
compose/karmen/data/appdata_occakkp6o8zz/js/gallery/scripts-for-file-app.js.gzip


BIN
compose/karmen/data/appdata_occakkp6o8zz/preview/14/256-256-crop.png


BIN
compose/karmen/data/appdata_occakkp6o8zz/preview/14/500-500-max.png


+ 1 - 0
compose/karmen/data/appdata_occakkp6o8zz/theming/0/icon-core-filetypes_text.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path fill="#969696" d="m2.5 1c-0.28 0-0.5 0.22-0.5 0.5v13c0 0.28 0.22 0.5 0.5 0.5h11c0.28 0 0.5-0.22 0.5-0.5v-10.5l-3-3h-8.5zm1.5 2h6v1h-6v-1zm0 3h5v1h-5v-1zm0 3h8v1h-8v-1zm0 3h4v1h-4v-1z"/></svg>

+ 0 - 0
compose/karmen/data/index.html


BIN
compose/karmen/data/nextcloud.db


BIN
compose/karmen/data/nextcloud.db-shm


+ 0 - 0
compose/karmen/data/nextcloud.db-wal


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
compose/karmen/data/nextcloud.log


+ 1 - 0
compose/karmen/docker-compose.yml

@@ -55,6 +55,7 @@ services:
       - NEXTCLOUD_TRUSTED_DOMAINS=nextcloud_server
     volumes:
       - ./data:/var/www/html/data
+      - ./config:/var/www/html/config
     ports:
       - 8080:80
 

+ 4 - 11
orm/document.go

@@ -14,17 +14,10 @@ import (
 type Document struct {
 	gorm.Model
 
-	Name       string
-	InputPath  string
-	OutputPath string
-
-	CloudUrl      string
-	CloudUsername string
-	CloudPassword string
+	Name string
 
-	LimeSurveyUrl      string
-	LimeSurveyUsername string
-	LimeSurveyPassword string
+	OutputPath string
+	CloudPath  string
 
 	Jobs []*Job
 
@@ -35,7 +28,7 @@ type Document struct {
 }
 
 type DocumentForUpdate struct {
-	Document Document
+	Document
 }
 
 type DocumentForAdd struct{}

+ 7 - 2
templates/documents_add_update.html.tpl

@@ -6,7 +6,7 @@
   {{if .Options.Get "update"}}
   <ol class="breadcrumb">
     <li class="breadcrumb-item"><a href="/documents?{{query "tpl_layout" "base" "tpl_content" "documents"}}">Documento</a></li>
-    <li class="breadcrumb-item active"><a href="#">Aggiorna document</a></li>
+    <li class="breadcrumb-item active"><a href="#">Aggiorna documento</a></li>
   </ol>
   </nav>
   
@@ -42,7 +42,12 @@
       <label class="control-label" for="document_name">Nome</label>
       <input type="text" name="Name" class="form-control" id="document_name" placeholder="Nome" {{if .Options.Get "update"}} value="{{.Data.Name}}" {{end}} required>
     </div>
-        
+
+    <div class="form-group has-feedback">
+      <label class="control-label" for="document_cloud_path">Cartella di lavoro remota</label>
+      <input type="text" name="CloudPath" class="form-control" id="document_cloud_path" placeholder="Documents/" {{if .Options.Get "update"}} value="{{.Data.CloudPath}}" {{end}} required>
+    </div>
+
     <div class="form-group">
       <button type="submit" class="btn btn-primary">Salva</button>
       {{if .Options.Get "update"}}

+ 3 - 3
watch.sh

@@ -4,9 +4,9 @@ diskFree=$(df -h /dev/sda1 | awk 'NR>1{print $4}')
 value="${diskFree::-1}"
 
 while inotifywait -r -e modify ./; do
-  if (( value < 2 )); then
-        docker image prune -a -f
-  fi
+  # if (( value < 2 )); then
+  #       docker image prune -a -f
+  # fi
   docker-compose -f compose/karmen/docker-compose.yml down
   docker-compose -f compose/karmen/docker-compose.yml up --build -d
 done

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff