From cd43f6dd68cf49a35c41e7e1889a1b88af4e9ff5 Mon Sep 17 00:00:00 2001
From: Christina Toegl <toegl@tugraz.at>
Date: Sun, 7 Nov 2021 20:13:07 +0100
Subject: [PATCH] Add support for favorites and recent files; Update webdav
 library

---
 package.json                                  |   3 +
 packages/file-handling/README.md              |   6 +-
 packages/file-handling/package.json           |   2 +-
 packages/file-handling/src/file-sink.js       |   3 +
 packages/file-handling/src/file-source.js     |   3 +
 .../src/i18n/de/translation.json              |   6 +-
 .../src/i18n/en/translation.json              |   6 +-
 .../src/nextcloud-file-picker.js              | 500 +++++++++++++++++-
 yarn.lock                                     |  60 ++-
 9 files changed, 539 insertions(+), 50 deletions(-)

diff --git a/package.json b/package.json
index 42030b6c..15faba24 100644
--- a/package.json
+++ b/package.json
@@ -24,5 +24,8 @@
   "license": "LGPL-2.1-or-later",
   "devDependencies": {
     "lerna": "^4.0.0"
+  },
+  "dependencies": {
+    "webdav": "4.6.0"
   }
 }
diff --git a/packages/file-handling/README.md b/packages/file-handling/README.md
index 30e19ea6..87694b0b 100644
--- a/packages/file-handling/README.md
+++ b/packages/file-handling/README.md
@@ -95,6 +95,8 @@ files from a [Nextcloud](https://nextcloud.com/) instance or to a dbp-clipboard.
   This is supported by the provider! Use this object to sync file source and file sink on one page at first time open.
     - example `<dbp-file-source initial-file-handling-state="{target: 'local', path:'my/server/path'}"></dbp-file-source>`
     - example provider `<dbp-file-source subscribe="initial-file-handling-state"></dbp-file-source>`
+- `show-nextcloud-favorites` (optional): Needs to be set to show the favorites icon
+    - example `show-nextcloud-favorites`
 
 ### Emitted attributes
 
@@ -160,7 +162,9 @@ files to a [Nextcloud](https://nextcloud.com/) instance or to a dbp-clipboard.
   This is supported by the provider! Use this object to sync file source and file sink on one page at first time open.
   - example `<dbp-file-source initial-file-handling-state="{target: 'local', path:'my/server/path'}"></dbp-file-source>`
   - example provider `<dbp-file-source subscribe="initial-file-handling-state"></dbp-file-source>`
-
+- `show-nextcloud-additional-menu` (optional): Needs to be set to show the additional menu
+    - example `show-nextcloud-additional-menu`
+    
 ### Emitted attributes
 
 The component emits a `dbp-set-property` event for the attribute `initial-file-handling-state`:
diff --git a/packages/file-handling/package.json b/packages/file-handling/package.json
index 043b9b15..66f82b21 100644
--- a/packages/file-handling/package.json
+++ b/packages/file-handling/package.json
@@ -42,7 +42,7 @@
     "lit": "^2.0.0",
     "material-design-icons-svg": "^3.0.0",
     "tabulator-tables": "^4.8.4",
-    "webdav": "^3.6.1"
+    "webdav": "4.6.0"
   },
   "scripts": {
     "clean": "rm dist/*",
diff --git a/packages/file-handling/src/file-sink.js b/packages/file-handling/src/file-sink.js
index 4e9282d1..f66fa2bb 100644
--- a/packages/file-handling/src/file-sink.js
+++ b/packages/file-handling/src/file-sink.js
@@ -38,6 +38,7 @@ export class FileSink extends ScopedElementsMixin(DbpFileHandlingLitElement) {
         this.firstOpen = true;
         this.fullsizeModal = false;
         this.nextcloudAuthInfo = '';
+        this.showNextcloudAdditionalMenu = false;
 
         this.initialFileHandlingState = {target: '', path: ''};
     }
@@ -74,6 +75,7 @@ export class FileSink extends ScopedElementsMixin(DbpFileHandlingLitElement) {
             firstOpen: {type: Boolean, attribute: false},
             nextcloudPath: {type: String, attribute: false},
             fullsizeModal: { type: Boolean, attribute: 'fullsize-modal' },
+            showNextcloudAdditionalMenu: { type: Boolean, attribute: 'show-nextcloud-additional-menu' },
             initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
         };
     }
@@ -304,6 +306,7 @@ export class FileSink extends ScopedElementsMixin(DbpFileHandlingLitElement) {
                    directory-path="${this.nextcloudPath}"
                    nextcloud-file-url="${this.nextcloudFileURL}"
                    ?store-nextcloud-session="${this.nextcloudStoreSession}"
+                   ?show-nextcloud-additional-menu="${this.showNextcloudAdditionalMenu}"
                    @dbp-nextcloud-file-picker-file-uploaded="${(event) => {
                        this.uploadToNextcloud(event.detail);
                    }}"
diff --git a/packages/file-handling/src/file-source.js b/packages/file-handling/src/file-source.js
index fdee6df2..8aed74ef 100644
--- a/packages/file-handling/src/file-source.js
+++ b/packages/file-handling/src/file-source.js
@@ -59,6 +59,7 @@ export class FileSource extends ScopedElementsMixin(DbpFileHandlingLitElement) {
         this.nextcloudAuthInfo = '';
         this.maxFileSize = '';
         this.multipleFiles = Number.MAX_VALUE;
+        this.showNextcloudAdditionalMenu = false;
 
         this.initialFileHandlingState = {target: '', path: ''};
     }
@@ -95,6 +96,7 @@ export class FileSource extends ScopedElementsMixin(DbpFileHandlingLitElement) {
             isDialogOpen: { type: Boolean, attribute: 'dialog-open' },
             maxFileSize: { type: Number, attribute: 'max-file-size'},
             multipleFiles: { type: Number, attribute: 'number-of-files'},
+            showNextcloudAdditionalMenu: { type: Boolean, attribute: 'show-nextcloud-additional-menu' },
 
             initialFileHandlingState: {type: Object, attribute: 'initial-file-handling-state'},
         };
@@ -526,6 +528,7 @@ export class FileSource extends ScopedElementsMixin(DbpFileHandlingLitElement) {
                    auth-info="${this.nextcloudAuthInfo}"
                    allowed-mime-types="${this.allowedMimeTypes}"
                    max-selected-items="${this.multipleFiles}"
+                   ?show-nextcloud-additional-menu="${this.showNextcloudAdditionalMenu}"
                    @dbp-nextcloud-file-picker-file-downloaded="${(event) => {
                     this.sendFileEvent(event.detail.file, event.detail.maxUpload);}}">
                 </dbp-nextcloud-file-picker>`;
diff --git a/packages/file-handling/src/i18n/de/translation.json b/packages/file-handling/src/i18n/de/translation.json
index 3b866e64..514568dc 100644
--- a/packages/file-handling/src/i18n/de/translation.json
+++ b/packages/file-handling/src/i18n/de/translation.json
@@ -72,7 +72,11 @@
     "abort-message": "Vorgang wurde abgebrochen.",
     "remember-me": "Mit {{name}} verbunden bleiben",
     "log-out": "Verbindung trennen",
-    "open-submenu": "Untermenü öffnen"
+    "open-submenu": "Untermenü öffnen",
+    "error-save-to-favorites": "Speichern in Favoriten nicht möglich! Bitte wählen Sie einen Ordner innerhalb der Favoriten aus.",
+    "error-save-to-recent": "Speichern in den neuesten Dateien nicht möglich! Bitte wählen Sie einen Ordner innerhalb der neuesten Dateien aus.",
+    "recent-files-link-text": "Neueste Dateien",
+    "favorites-link-text": "Meine Favoriten"
   },
   "clipboard": {
     "add-files": "Dateien der Zwischenablage hinzufügen",
diff --git a/packages/file-handling/src/i18n/en/translation.json b/packages/file-handling/src/i18n/en/translation.json
index 999d879b..898223ee 100644
--- a/packages/file-handling/src/i18n/en/translation.json
+++ b/packages/file-handling/src/i18n/en/translation.json
@@ -73,7 +73,11 @@
     "abort-message": "The process was canceled.",
     "remember-me": "Stay connected with {{name}}",
     "log-out": "Disconnect",
-    "open-submenu": "Open submenu"
+    "open-submenu": "Open submenu",
+    "error-save-to-favorites": "Saving to Favorites not possible! Please select a folder within the Favorites.",
+    "error-save-to-recent": "Saving to Recent Files not possible! Please select a folder within the Recent Files.",
+    "recent-files-link-text": "Recent Files",
+    "favorites-link-text": "My Favorites"
   },
   "clipboard": {
     "add-files": "Add files to clipboard",
diff --git a/packages/file-handling/src/nextcloud-file-picker.js b/packages/file-handling/src/nextcloud-file-picker.js
index 4e262ceb..f5e710e9 100644
--- a/packages/file-handling/src/nextcloud-file-picker.js
+++ b/packages/file-handling/src/nextcloud-file-picker.js
@@ -5,8 +5,8 @@ import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
 import {Icon, MiniSpinner} from '@dbp-toolkit/common';
 import * as commonUtils from '@dbp-toolkit/common/utils';
 import * as commonStyles from '@dbp-toolkit/common/styles';
-import {createClient} from 'webdav/web';
 import {classMap} from 'lit/directives/class-map.js';
+import {createClient, parseXML, parseStat} from 'webdav/web';
 import {humanFileSize} from '@dbp-toolkit/common/i18next';
 import Tabulator from 'tabulator-tables';
 import MicroModal from './micromodal.es';
@@ -14,6 +14,7 @@ import {name as pkgName} from './../package.json';
 import * as fileHandlingStyles from './styles';
 import {encrypt, decrypt, parseJwt} from './crypto.js';
 
+
 /**
  * NextcloudFilePicker web component
  */
@@ -57,11 +58,14 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
         this.abortUpload = false;
         this.authInfo = '';
         this.selectBtnDisabled = true;
-
         this.storeSession = false;
         this.showSubmenu = false;
         this.bounCloseSubmenuHandler = this.closeSubmenu.bind(this);
         this.initateOpensubmenu = false;
+        this.showAdditionalMenu = false;
+        this.isInFavorites = false;
+        this.isInRecent = false;
+        this.userName = '';
     }
 
     static get scopedElements() {
@@ -100,7 +104,9 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
             abortUploadButton: {type: Boolean, attribute: false},
             selectBtnDisabled: {type: Boolean, attribute: true},
             storeSession: {type: Boolean, attribute: 'store-nextcloud-session'},
-            showSubmenu: {type: Boolean, attribute: false}
+            showSubmenu: {type: Boolean, attribute: false},
+            showAdditionalMenu: { type: Boolean, attribute: 'show-nextcloud-additional-menu' },
+            userName: { type: Boolean, attribute: false },
         };
 
     }
@@ -535,8 +541,311 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                 }
 
                 this.loadDirectory(this.directoryPath);
+                this.userName = data.loginName;
+            }
+        }
+    }
+
+    /**
+     * 
+     * @param {*} data 
+     * @returns reduced list of objects, including users files
+     */
+    filterUserFilesOnly(data) { //TODO verify
+        // R = Share, S = Shared Folder, M = Group folder or external source, G = Read, D = Delete, NV / NVW = Write, CK = Create
+        let result = [];
+
+        for (let i = 0; i < data.length; i++) {
+            if (data) {
+                let file_perm = data[i].props.permissions;
+                if (!file_perm.includes('M') && !file_perm.includes('S')) {
+                    result.push(data[i]);
+                }
             }
         }
+        return result;
+    }
+
+    /**
+     * 
+     * @param {*} path 
+     * @returns array including file path and base name
+     */
+    parseFileAndBaseName(path) {
+        if (path[0] !== "/") { //TODO verify
+            path = "/" + path;
+        }
+        while (/^.+\/$/.test(path)) {
+            path = path.substr(0, path.length - 1);
+        }
+        path = decodeURIComponent(path);
+
+        let array1 = this.webDavUrl.split('/');
+        let array2 = path.split('/');
+        for (let i = 0; i < array2.length; i++) {
+            let item2 = array2[i];
+            array1.forEach(item1 => {
+                if (item1 === item2) {
+                    array2.shift();
+                    i--;
+                }
+            });
+        }
+        array2.shift();
+
+        let basename = array2[array2.length - 1];
+        let filename = '/' + array2.join('/');
+        
+        return [ filename, basename ];
+    }
+
+    /**
+     * 
+     * @param {*} response 
+     * @returns list of file objects containing corresponding information
+     */
+    mapResponseToObject(response) {
+        let results = [];
+
+        response.forEach(item => {
+            const [ filePath, baseName ] = this.parseFileAndBaseName(item.href);
+
+            const prop = item.propstat.prop;
+            let etag = typeof prop.getetag === 'string' ? prop.getetag.replace(/"/g, '') : null;
+            let sizeVal = prop.getcontentlength ? prop.getcontentlength : '0';
+            let fileType = prop.resourcetype && typeof prop.resourcetype === 'object' && typeof prop.resourcetype.collection !== 'undefined' ? 'directory' : 'file';
+            
+            let mimeType;
+            if (fileType === 'file') {
+                mimeType = prop.getcontenttype && typeof prop.getcontenttype === 'string' ? prop.getcontenttype.split(';')[0] : '';
+            }
+
+            let propsObject =  { getetag: etag, getlastmodified: prop.getlastmodified, getcontentlength: sizeVal, 
+                                permissions: prop.permissions, resourcetype: fileType, getcontenttype: prop.getcontenttype };
+
+            let statObject = { basename: baseName, etag: etag, filename: filePath, lastmod: prop.getlastmodified, 
+                mime: mimeType, props: propsObject, size: parseInt(sizeVal, 10), type: fileType };
+
+            results.push(statObject);
+        });
+
+        return results;
+    }
+    
+    /**
+     * Loads the favorites from WebDAV
+     *
+     */
+    loadFavorites() {
+        this.hideMoreMenu();
+        const i18n = this._i18n;
+
+        if (typeof this.directoryPath === 'undefined' || this.directoryPath === undefined) {
+            this.directoryPath = '';
+        }
+
+        console.log("load nextcloud favorites");
+        this.selectAllButton = true;
+        this.loading = true;
+        this.statusText = i18n.t('nextcloud-file-picker.loadpath-nextcloud-file-picker', {name: this.nextcloudName});
+        this.lastDirectoryPath = this.directoryPath;
+        this.directoryPath = '';
+        this.isInRecent = false;
+        this.isInFavorites = true;
+
+        if (this.webDavClient === null) {
+            // client is broken reload try to reset & reconnect
+            this.tabulatorTable.clearData();
+            this.webDavClient = null;
+            let reloadButton = html`${i18n.t('nextcloud-file-picker.something-went-wrong')} <button class="button"
+                            title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
+                            @click="${async () => { this.openFilePicker(); } }"><dbp-icon name="reload"></button>`;
+            this.loading = false;
+            this.statusText = reloadButton;
+        }
+
+        //see https://github.com/perry-mitchell/webdav-client#customRequest
+        this.webDavClient
+            .customRequest('/', {method: 'REPORT', responseType: "text/xml", details: true, data: "<?xml version=\"1.0\"?>" +
+                    "   <oc:filter-files  xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">" +
+                    "       <oc:filter-rules>" +
+                    "           <oc:favorite>1</oc:favorite>" +
+                    "       </oc:filter-rules>" +
+                    "       <d:prop>" +
+                    "           <d:getlastmodified />" +
+                    "           <d:resourcetype />" +
+                    "           <d:getcontenttype />" +
+                    "           <d:getcontentlength />" +
+                    "           <d:getetag />" +
+                    "           <oc:permissions />" +
+                    "        </d:prop>" +
+                    "   </oc:filter-files>"
+                })
+            .then(contents => {
+                parseXML(contents.data).then(davResp => {
+                    // console.log("-contents.data-----", davResp);
+                    let dataObject = this.mapResponseToObject(davResp.multistatus.response);
+
+                    this.loading = false;
+                    this.statusText = "";
+                    this.tabulatorTable.setData(dataObject);
+                    this.isPickerActive = true;
+                    this._(".nextcloud-content").scrollTop = 0;
+                    this._("#download-button").setAttribute("disabled", "true");
+                });
+            }).catch(error => { //TODO verify error catching
+                console.error(error.message);
+
+                // on Error: try to reload with home directory
+                if (this.webDavClient !== null && error.message.search("401") === -1) {
+                    console.log("error in load directory");
+                    this.directoryPath = "";
+                    this.loadDirectory("");
+                }
+                else {
+                    this.loading = false;
+                    this.statusText = html`<span class="error"> ${i18n.t('nextcloud-file-picker.webdav-error', {error: error.message})} </span>`;
+                    this.isPickerActive = false;
+                    this.tabulatorTable.clearData();
+                    this.webDavClient = null;
+                    let reloadButton = html`${i18n.t('nextcloud-file-picker.something-went-wrong')} <button class="button"
+                                title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
+                                @click="${async () => { this.openFilePicker(); } }"><dbp-icon name="reload"></button>`;
+                    this.loading = false;
+                    this.statusText = reloadButton;
+                }
+                this.isInFavorites = false;
+        });
+    }
+
+    /**
+     * Loads recent files and folders from WebDAV
+     *
+     */
+    loadRecent() {
+        this.hideMoreMenu();
+        const i18n = this._i18n;
+
+        if (typeof this.directoryPath === 'undefined' || this.directoryPath === undefined) {
+            this.directoryPath = '';
+        }
+
+        console.log("load recent files");
+        this.selectAllButton = true;
+        this.loading = true;
+        this.statusText = i18n.t('nextcloud-file-picker.loadpath-nextcloud-file-picker', {name: this.nextcloudName});
+        this.lastDirectoryPath = this.directoryPath;
+        this.directoryPath = '';
+        this.isInFavorites = false;
+        this.isInRecent = true;
+
+        let date = new Date();
+        date.setMonth(date.getMonth() - 3);
+        let searchDate = date.toISOString().split('.')[0] + 'Z';
+
+        if (this.webDavClient === null) {
+            // client is broken reload try to reset & reconnect
+            this.tabulatorTable.clearData();
+            this.webDavClient = null;
+            let reloadButton = html`${i18n.t('nextcloud-file-picker.something-went-wrong')} <button class="button"
+                            title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
+                            @click="${async () => { this.openFilePicker(); } }"><dbp-icon name="reload"></button>`;
+            this.loading = false;
+            this.statusText = reloadButton;
+        }
+
+        //see https://github.com/perry-mitchell/webdav-client#customRequest
+        this.webDavClient
+            .customRequest('../..', { method: 'SEARCH', responseType: "text/xml", headers: { 'Content-Type': "text/xml" }, details: true, data: "<?xml version=\"1.0\" encoding='UTF-8'?>" +
+                "   <d:searchrequest xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">" +
+                "       <d:basicsearch>" +
+                "           <d:select>" +
+                "               <d:prop>" +
+                "                   <d:getlastmodified />" +
+                "                   <d:resourcetype />" +
+                "                   <d:getcontenttype />" +
+                "                   <d:getcontentlength />" +
+                "                   <d:getetag />" +
+                "                   <oc:permissions />" +
+                "                   <oc:size/>"+
+                "                   <oc:owner-id/>" +
+                "                   <oc:owner-display-name/>" +
+                "               </d:prop>" +
+                "           </d:select>" +
+                "           <d:from>" +
+                "               <d:scope>" +
+                "                   <d:href>/files/" + this.userName + "/</d:href>" +
+                "                   <d:depth>infinity</d:depth>" +
+                "               </d:scope>" + 
+                "           </d:from>" +
+                "           <d:where> " +
+                "               <d:gte>" +
+                "                   <d:prop>" +
+                "                      <d:getlastmodified/>" +
+                "                   </d:prop>" +
+                "                   <d:literal>" + searchDate + "</d:literal>" +
+                "               </d:gte>" +
+                "           </d:where>" +
+                "           <d:orderby>" +
+                "               <d:order>" +
+                "                   <d:prop>" +
+                "                       <d:getlastmodified/>" +
+                "                   </d:prop>" +
+                "                   <d:descending/>" +
+                "               </d:order>" +
+                "           </d:orderby>" +
+                "           <d:limit>"+
+                "               <d:nresults>100</d:nresults>" +
+                "           </d:limit>"+
+                "       </d:basicsearch>" +
+                "   </d:searchrequest>"
+            })
+            .then(contents => {
+                parseXML(contents.data).then(davResp => {
+                    console.log('davResp', davResp);
+
+                    let dataObject = this.mapResponseToObject(davResp.multistatus.response);
+                    console.log("-contents.data-----", dataObject);
+
+                    if (this._("#user_files_only") && this._("#user_files_only").checked) {
+                        dataObject = this.filterUserFilesOnly(dataObject);
+                        // console.log('show only my files');
+                    }
+
+                    this.loading = false;
+                    this.statusText = "";
+                    this.tabulatorTable.setData(dataObject);
+                    this.tabulatorTable.setSort([
+                        {column: "lastmod", dir: "desc"}
+                    ]);
+                    this.isPickerActive = true;
+                    this._(".nextcloud-content").scrollTop = 0;
+                    this._("#download-button").setAttribute("disabled", "true");
+                });
+            }).catch(error => {
+                console.error(error.message);
+
+                // on Error: try to reload with home directory
+                if (this.webDavClient !== null && error.message.search("401") === -1) {
+                    console.log("error in load directory");
+                    this.directoryPath = "";
+                    this.loadDirectory("");
+                }
+                else {
+                    this.loading = false;
+                    this.statusText = html`<span class="error"> ${i18n.t('nextcloud-file-picker.webdav-error', {error: error.message})} </span>`;
+                    this.isPickerActive = false;
+                    this.tabulatorTable.clearData();
+                    this.webDavClient = null;
+                    let reloadButton = html`${i18n.t('nextcloud-file-picker.something-went-wrong')} <button class="button"
+                                title="${i18n.t('nextcloud-file-picker.refresh-nextcloud-file-picker')}"
+                                @click="${async () => { this.openFilePicker(); } }"><dbp-icon name="reload"></button>`;
+                    this.loading = false;
+                    this.statusText = reloadButton;
+                }
+
+                this.isInRecent = false;
+        });
     }
 
     toggleCollapse(e) {
@@ -603,11 +912,12 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                     "</d:propfind>"
             })
             .then(contents => {
-
                 this.loading = false;
                 this.statusText = "";
                 this.tabulatorTable.setData(contents.data);
                 this.isPickerActive = true;
+                this.isInFavorites = false;
+                this.isInRecent = false;
                 this._(".nextcloud-content").scrollTop = 0;
                 if (!this.activeDirectoryRights.includes("CK") && !this.activeDirectoryRights.includes("NV")) {
                     this._("#download-button").setAttribute("disabled", "true");
@@ -731,6 +1041,14 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
      * @param directory
      */
     sendDirectory(directory) {
+        if (this.isInFavorites) {
+            this.statusText = html`<span class="error"> ${ i18n.t('nextcloud-file-picker.error-save-to-favorites') } </span>`;
+            return;
+        } else if (this.isInRecent) { //TODO verify
+            this.statusText = html`<span class="error"> ${ i18n.t('nextcloud-file-picker.error-save-to-recent') } </span>`;
+            return;
+        }
+        
         const i18n = this._i18n;
         this.tabulatorTable.deselectRow();
         let path;
@@ -1297,6 +1615,39 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
         return this.nextcloudFileURL + this.directoryPath;
     }
 
+    toggleMoreMenu() {
+        const menu = this.shadowRoot.querySelector("ul.extended-menu");
+        const menuStart = this.shadowRoot.querySelector("a.extended-menu-link");
+
+        if (menu === null || menuStart === null) {
+            return;
+        }
+
+        menu.classList.toggle('hidden');
+
+        if (this.menuHeight === -1) {
+            this.menuHeight = menu.clientHeight;
+        }
+
+        let topValue = menuStart.getBoundingClientRect().bottom;
+        let isMenuOverflow = this.menuHeight + topValue >= window.innerHeight ? true : false;
+        
+        if (isMenuOverflow && !menu.classList.contains('hidden')) {
+            menu.setAttribute('style', 'position: fixed;top: ' + topValue + 'px;bottom: 0;border-bottom: 0;overflow-y: auto;');
+            menu.scrollTop = 0;
+            document.body.setAttribute('style', 'overflow:hidden;');
+        } else if (isMenuOverflow && menu.classList.contains('hidden')) {
+            document.body.removeAttribute('style', 'overflow:hidden;');
+            menu.removeAttribute('style');
+        }
+    }
+
+    hideMoreMenu() {
+        const menu = this.shadowRoot.querySelector("ul.extended-menu");
+        if (menu && !menu.classList.contains('hidden'))
+            this.toggleMoreMenu();
+    }
+
     static get styles() {
         // language=css
         return css`
@@ -1306,6 +1657,7 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
             ${commonStyles.getModalDialogCSS()}
             ${commonStyles.getRadioAndCheckboxCss()}
             ${fileHandlingStyles.getFileHandlingCss()}
+
             .visible {
                 display: unset;
             }
@@ -1319,6 +1671,56 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                 color: var(--dbp-danger-dark);
             }
 
+            .filter-options-wrapper {
+                padding-right: 0px;
+                padding-top: 10px;
+                padding-bottom: 10px;
+                padding-left: 0px;
+            }
+
+            .extended-menu li {
+                padding: 7px;
+                padding-right: 46px;
+                border-bottom: 1px solid #f3f3f3;
+            }
+
+            .extended-menu li.active {
+                background-color: var(--dbp-dark);
+                color: var(--dbp-light);
+            }
+            .extended-menu li.active a:hover {
+                color: var(--dbp-light);
+            }
+
+            .extended-menu a.inactive {
+                color: var(--dbp-muted-text);
+                pointer-events: none;
+                cursor: default;
+            }
+
+            .extended-menu a {
+                padding: 8px;
+            }
+
+            .extended-menu {
+                list-style: none;
+                border: black 1px solid;
+                position: absolute;
+                background-color: white;
+                z-index: 1000;
+                right: 0px;
+            }
+
+            .extended-menu a:hover {
+                color: #E4154B;
+            }
+
+            ul.extended-menu li.close {
+                display: block;
+                padding: 7px 15px 7px 15px;
+                text-align: right;
+                cursor: pointer;
+            }
 
             .nextcloud-header {
                 margin-bottom: 2rem;
@@ -1409,10 +1811,11 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
             }
 
 
-            .add-folder {
+            .additional-menu {
                 white-space: nowrap;
                 align-self: end;
                 height: 33px;
+                margin-right: 5px;
             }
 
             .nextcloud-nav p {
@@ -1658,24 +2061,34 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                 left: 7px;
             }
 
+            .more-menu {
+                height: 22.4px;
+                width: 22.4px;
+                top: 8px;
+            }
+
+            .nextcloud-nav a.home-link {
+                font-size: 1.4em;
+            }
 
             @media only screen
             and (orientation: portrait)
             and (max-width: 768px) {
-                .add-folder button {
+                .additional-menu button {
                     float: right;
                 }
 
-                .add-folder {
+                .additional-menu {
                     position: absolute;
                     right: 0px;
+                    margin-right: 10px;
                 }
 
-                .nextcloud-nav {
-                    display: block;
-                }
+                /* .nextcloud-nav {
+                    display: block; TODO verify if this is enough
+                } */
 
-                .add-folder {
+                .additional-menu {
                     position: inherit;
                 }
 
@@ -1848,29 +2261,53 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                 <div class="nextcloud-content ${classMap({hidden: !this.isPickerActive})}">
                     <div class="nextcloud-nav">
                         <p>${this.getBreadcrumb()}</p>
-                        <div class="menu-buttons">
-                            <div class="add-folder ${classMap({hidden: !this.directoriesOnly})}">
-                            <div class="inline-block">
-                                <div id="new-folder-wrapper" class="hidden">
-                                    <input type="text"
-                                           placeholder="${i18n.t('nextcloud-file-picker.new-folder-placeholder')}"
-                                           name="new-folder" class="input" id="new-folder"/>
-                                    <button class="button add-folder-button"
-                                            title="${i18n.t('nextcloud-file-picker.add-folder')}"
-                                            @click="${() => {
-                                                this.addFolder();
-                                            }}">
-                                        <dbp-icon name="checkmark-circle" class="nextcloud-add-folder"></dbp-icon>
-                                    </button>
+
+                        <div class="additional-menu ${classMap({hidden: !this.showAdditionalMenu})}">
+                            
+                            <a class="extended-menu-link" @click="${() => { this.toggleMoreMenu(); }}" title="${i18n.t('nextcloud-file-picker.more-menu')}">
+                                <dbp-icon name="more-filled" class="more-menu"></dbp-icon>
+                            </a>
+                            <ul class='extended-menu hidden'>
+                                <li class="${classMap({active: this.isInFavorites})}" id="favorites-item">
+                                    <a class="" @click="${this.loadFavorites}">
+                                        ${i18n.t('nextcloud-file-picker.favorites-link-text')}
+                                    </a>
+                                </li>
+                                <li class="${classMap({active: this.isInRecent})}" id="recent-item">
+                                    <a class="" @click="${this.loadRecent}">
+                                        ${i18n.t('nextcloud-file-picker.recent-files-link-text')}
+                                    </a>
+                                </li>
+                                <li class="${classMap({hidden: !this.directoriesOnly})}">
+                                    <a class="${classMap({inactive: this.isInRecent || this.isInFavorites})}" @click="${() => { this.openAddFolderDialogue(); }}">
+                                        ${i18n.t('nextcloud-file-picker.add-folder')}
+                                    </a>
+                                </li>
+                            
+                                <div class="inline-block">
+                                    <div id="new-folder-wrapper" class="hidden">
+                                        <input type="text"
+                                            placeholder="${i18n.t('nextcloud-file-picker.new-folder-placeholder')}"
+                                            name="new-folder" class="input" id="new-folder"/>
+                                        <button class="button add-folder-button"
+                                                title="${i18n.t('nextcloud-file-picker.add-folder')}"
+                                                @click="${() => {
+                                                    this.addFolder();
+                                                }}">
+                                            <dbp-icon name="checkmark-circle" class="nextcloud-add-folder"></dbp-icon>
+                                        </button>
+                                    </div>
                                 </div>
-                            </div>
-                            <button class="button"
+                            <!-- <button class="button ${classMap({hidden: this.showAdditionalMenu})}"
                                     title="${i18n.t('nextcloud-file-picker.add-folder-open')}"
                                     @click="${() => {
                                         this.openAddFolderDialogue();
                                     }}">
                                 <dbp-icon name="plus" class="nextcloud-add-folder" id="add-folder-button"></dbp-icon>
-                            </button>
+                            </button> -->
+                                <li class="close" @click="${this.hideMoreMenu}"><dbp-icon name="close" style="color: red"></dbp-icon></li>
+                            </ul>
+                        
                         </div>
                             <div id="submenu" class="${classMap({hidden: !this.storeSession})}"
                                 title="${i18n.t('nextcloud-file-picker.open-submenu')}"
@@ -1890,6 +2327,13 @@ export class NextcloudFilePicker extends ScopedElementsMixin(DBPLitElement) {
                             </div>
                         </div>
                     </div>
+                    <div class="filter-options-wrapper ${classMap({hidden: !this.isInRecent})}">
+                        <label id="user_files_only_wrapper" class="button-container">
+                        <!-- ${i18n.t('nextcloud-file-picker.replace-mode-all')} --> Show only my files <!--TODO-->
+                            <input type="checkbox" id="user_files_only" name="user_files_only" value="user_files_only" > <!--@click="${() => { this.filterUserFilesOnly(); }}"-->
+                            <span class="checkmark" id="user_files_only_checkmark"></span>
+                        </label>
+                    </div>
                     <div class="table-wrapper">
                         <table id="directory-content-table" class="force-no-select"></table>
                     </div>
diff --git a/yarn.lock b/yarn.lock
index afeaef36..49691d33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2792,6 +2792,11 @@ chardet@^0.7.0:
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
 
+charenc@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+  integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
+
 chokidar@3.5.2, chokidar@^3.4.0, chokidar@^3.5.1:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
@@ -3205,6 +3210,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+crypt@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+  integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
+
 custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -3960,12 +3970,10 @@ fast-levenshtein@^2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
-fast-xml-parser@^3.17.4:
-  version "3.21.1"
-  resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736"
-  integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==
-  dependencies:
-    strnum "^1.0.4"
+fast-xml-parser@^3.19.0:
+  version "3.19.0"
+  resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz"
+  integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
 
 fastq@^1.6.0:
   version "1.13.0"
@@ -4851,7 +4859,7 @@ is-boolean-object@^1.1.0:
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-buffer@^1.1.5:
+is-buffer@^1.1.5, is-buffer@~1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@@ -5395,6 +5403,11 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
+layerr@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/layerr/-/layerr-0.1.2.tgz#16c8e7fb042d3595ab15492bdad088f31d7afd15"
+  integrity sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==
+
 lazystream@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638"
@@ -5727,6 +5740,15 @@ material-design-icons-svg@^3.0.0:
   resolved "https://registry.yarnpkg.com/material-design-icons-svg/-/material-design-icons-svg-3.2.0.tgz#0f669dbea24d10403ca5ffe9828deb9d4acd8e7e"
   integrity sha512-5YECqik/lDKRjyo5ItT5Jh12jdDM6ySULnhTyUKomELFsgrwM+2IMkHvXLw61n/q6bJ9tfK/suAkWiTv1/uB4g==
 
+md5@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
+  integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
+  dependencies:
+    charenc "0.0.2"
+    crypt "0.0.2"
+    is-buffer "~1.1.6"
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -8396,10 +8418,10 @@ url-join@^4.0.1:
   resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
   integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
 
-url-parse@^1.4.7:
-  version "1.5.3"
-  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862"
-  integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==
+url-parse@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b"
+  integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==
   dependencies:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
@@ -8580,21 +8602,23 @@ web-component-analyzer@~1.1.1:
     typescript "^3.8.3"
     yargs "^15.3.1"
 
-webdav@^3.6.1:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/webdav/-/webdav-3.6.2.tgz#76e3d8e950e80698a2f1db23ef2496888662cbeb"
-  integrity sha512-HFRiI1jluMSPQMVgxVD6VVYNtaglO53vHG0uf7Zec+wl0A1Mei2z8/IFgAAAJMUuEWAx2AfBD5lcWhAiYA9LUw==
+webdav@4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/webdav/-/webdav-4.6.0.tgz#b12d3631562ed4a7d665dd1757349682dfc8f441"
+  integrity sha512-amL/NeZ73xe8cNC+uqAF3mOC/j5dNoNWlZswYCd7DKxhnZM7dViAVSv6gdCYeGAETjPEZVubcKDDRwGKz9ShQQ==
   dependencies:
     axios "^0.21.1"
-    base-64 "^0.1.0"
-    fast-xml-parser "^3.17.4"
+    base-64 "^1.0.0"
+    fast-xml-parser "^3.19.0"
     he "^1.2.0"
     hot-patcher "^0.5.0"
+    layerr "^0.1.2"
+    md5 "^2.3.0"
     minimatch "^3.0.4"
     nested-property "^4.0.0"
     path-posix "^1.0.0"
     url-join "^4.0.1"
-    url-parse "^1.4.7"
+    url-parse "^1.5.1"
 
 webidl-conversions@^3.0.0:
   version "3.0.1"
-- 
GitLab