diff --git a/lib/classes/JsonApi/Schemas/Folder.php b/lib/classes/JsonApi/Schemas/Folder.php
index 1cd5ba55a5f7f98e943f94d7bc9ce32a440ace36..2c61cae4ba2b0459658615c2f7260a2d075e1bc1 100644
--- a/lib/classes/JsonApi/Schemas/Folder.php
+++ b/lib/classes/JsonApi/Schemas/Folder.php
@@ -32,10 +32,11 @@ class Folder extends SchemaProvider
             'mkdate' => date('c', $resource->mkdate),
             'chdate' => date('c', $resource->chdate),
 
-            'is-visible' => (bool) $resource->isVisible($user->id),
+            'is-visible'  => (bool) $resource->isVisible($user->id),
             'is-readable' => (bool) $resource->isReadable($user->id),
             'is-writable' => (bool) $resource->isWritable($user->id),
             'is-editable' => (bool) $resource->isEditable($user->id),
+            'is-empty'    => (bool) $resource->is_empty,
             'is-subfolder-allowed' => (bool) $resource->isSubfolderAllowed($user->id),
         ];
 
@@ -76,6 +77,9 @@ class Folder extends SchemaProvider
                 self::RELATIONSHIP_LINKS => [
                     Link::RELATED => $this->createLinkToResource($resource->owner),
                 ],
+                self::RELATIONSHIP_META => [
+                    'name' => $resource->owner->getFullName('no_title_rev'),
+                ]
             ];
         }
 
@@ -157,6 +161,9 @@ class Folder extends SchemaProvider
             self::RELATIONSHIP_LINKS => [
                 Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FOLDERS),
             ],
+            self::RELATIONSHIP_META => [
+                'count' => count($resource->subfolders)
+            ],
         ];
 
         return $relationships;
@@ -168,6 +175,9 @@ class Folder extends SchemaProvider
             self::RELATIONSHIP_LINKS => [
                 Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_FILE_REFS),
             ],
+            self::RELATIONSHIP_META => [
+                'count' => count($resource->file_refs)
+            ],
         ];
 
         return $relationships;
diff --git a/resources/vue/components/StudipFileChooser.vue b/resources/vue/components/StudipFileChooser.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e021aec3cad110db990c82e4acf4163e358d0614
--- /dev/null
+++ b/resources/vue/components/StudipFileChooser.vue
@@ -0,0 +1,149 @@
+<template>
+    <div class="file-chooser">
+        <button class="button" @click="openDialog">{{ buttonTitle }}</button><span>{{ selectedName }}</span>
+        <file-chooser-dialog v-if="showDialog" v-bind="$props" @close="closeDialog" @selected="select" />
+    </div>
+</template>
+
+<script>
+import FileChooserDialog from './file-chooser/FileChooserDialog.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'studip-file-chooser',
+    components: {
+        FileChooserDialog,
+    },
+
+    props: {
+        selectable: {
+            type: String,
+            default: 'file',
+            validator: (value) => {
+                return ['file', 'folder'].includes(value);
+            },
+        },
+        selectedId: {
+            type: String,
+            required: false,
+        },
+        courseId: {
+            type: String,
+            validator: (value) => {
+                return value !== '';
+            },
+            required: false,
+        },
+        userId: {
+            type: String,
+            validator: (value) => {
+                return value !== '';
+            },
+            required: false,
+        },
+        isImage: { type: Boolean, default: false },
+        isVideo: { type: Boolean, default: false },
+        isAudio: { type: Boolean, default: false },
+        isDocument: { type: Boolean, default: false },
+        excludedCourseFolderTypes: { type: Array, default: () => [] },
+        excludedUserFolderTypes: { type: Array, default: () => [] },
+    },
+    model: {
+        prop: 'selectedId',
+        event: 'select',
+    },
+    data() {
+        return {
+            showDialog: false,
+            selectedFile: null,
+            selectedFolder: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            fileById: 'file-refs/byId',
+            folderById: 'folders/byId',
+        }),
+        buttonTitle() {
+            if (this.selectable === 'folder') {
+                return this.$gettext('Ordner auswählen');
+            }
+
+            return this.$gettext('Datei auswählen');
+        },
+        selectedName() {
+            if (this.selectable === 'folder') {
+                if (this.selectedId === '') {
+                    return this.$gettext('Kein Ordner ausgewählt');
+                }
+                return this.$gettextInterpolate(this.$gettext('Ordner "%{folderName}" ausgewählt'), {
+                    folderName: this.folderById({ id: this.selectedId })?.attributes?.name ?? '-',
+                });
+            }
+
+            if (this.selectedId === '') {
+                return this.$gettext('Keine Datei ausgewählt');
+            }
+            return this.$gettextInterpolate(this.$gettext('Datei "%{fileName}" ausgewählt'), {
+                fileName: this.fileById({ id: this.selectedId })?.attributes?.name ?? '-',
+            });
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadFile: 'file-refs/loadById',
+            loadFolder: 'folders/loadById',
+        }),
+        openDialog() {
+            this.showDialog = true;
+        },
+        closeDialog() {
+            this.showDialog = false;
+        },
+        select(id) {
+            this.closeDialog();
+            this.$emit('select', id);
+        },
+        loadSelection() {
+            if (this.selectable === 'folder') {
+                if (this.selectedId !== '') {
+                    this.loadFolder({ id: this.selectedId });
+                }
+            } else {
+                if (this.selectedId !== '') {
+                    this.loadFile({ id: this.selectedId });
+                }
+            }
+        }
+    },
+    mounted() {
+        this.loadSelection();
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.file-chooser {
+    text-indent: 0;
+    max-width: 48em;
+    button {
+        margin: 0.5ex 0 0.5ex 0;
+        min-width: 140px;
+    }
+    span {
+        box-sizing: border-box;
+        border: solid thin var(--content-color-40);
+        border-left: none;
+        display: inline-block;
+        font-size: 14px;
+        line-height: 130%;
+        min-width: 100px;
+        width: calc(100% - 140px);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        padding: 5px 15px;
+        vertical-align: middle;
+        white-space: nowrap;
+    }
+}
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
index 9ca52b04dfaa6c514f304c11174dbc861a52e6f1..4789c9fa1096d9eee6dcca0a1fe5193c056cf966 100644
--- a/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareAudioBlock.vue
@@ -288,18 +288,14 @@
                     </label>
                     <label v-show="currentSource === 'studip_file'">
                         {{ $gettext('Datei') }}
-                        <courseware-file-chooser
-                            v-model="currentFileId"
-                            :isAudio="true"
-                            @selectFile="updateCurrentFile"
-                        />
+                        <studip-file-chooser v-model="currentFileId" selectable="file" :courseId="context.id" :userId="userId" :isAudio="true" :excludedCourseFolderTypes="excludedCourseFolderTypes" />
                     </label>
                     <label v-show="currentSource === 'studip_folder'">
                         {{ $gettext('Ordner') }}
-                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
+                        <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" />
                     </label>
                     <label v-show="currentSource === 'studip_folder'">
-                        {{ $gettext('Audio Aufnahmen zulassen') }}
+                        {{ $gettext('Audio-Aufnahmen zulassen') }}
                         <span
                             class="tooltip tooltip-icon"
                             :data-tooltip="$gettext('Um Aufnahmen zu ermöglichen, muss ein Ordner ausgewählt werden.')"
@@ -377,7 +373,6 @@ export default {
             fileRefById: 'file-refs/byId',
             relatedFileRefs: 'file-refs/related',
             urlHelper: 'urlHelper',
-            userId: 'userId',
             usersById: 'users/byId',
             relatedTermOfUse: 'terms-of-use/related',
         }),
diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
index 8d4e496403b5090352f3bf82a76d0701a47a2f81..65df25c4bcb5d79f0cbc86c61e40844f344aafee 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue
@@ -34,11 +34,7 @@
                             </label>
                             <label v-if="currentBeforeSource === 'studip'">
                                 {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
-                                    v-model="currentBeforeFileId"
-                                    :isImage="true"
-                                    @selectFile="updateCurrentBeforeFile"
-                                />
+                                <studip-file-chooser v-model="currentBeforeFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true" :excludedCourseFolderTypes="excludedCourseFolderTypes"/>
                             </label>
                         </form>
                     </courseware-tab>
@@ -57,11 +53,7 @@
                             </label>
                             <label v-if="currentAfterSource === 'studip'">
                                 {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
-                                    v-model="currentAfterFileId"
-                                    :isImage="true"
-                                    @selectFile="updateCurrentAfterFile"
-                                />
+                                <studip-file-chooser v-model="currentAfterFileId" selectable="file" :courseId="context.id" :userId="userId" :isImage="true"/>
                             </label>
                         </form>
                     </courseware-tab>
@@ -106,6 +98,7 @@ export default {
     },
     computed: {
         ...mapGetters({
+            fileRefById: 'file-refs/byId',
             viewMode: 'viewMode',
         }),
         beforeSource() {
@@ -204,14 +197,6 @@ export default {
             this.currentAfterFileId = this.afterFileId;
             this.currentAfterWebUrl = this.afterWebUrl;
         },
-        updateCurrentBeforeFile(file) {
-            this.currentBeforeFile = file;
-            this.currentBeforeFileId = file.id;
-        },
-        updateCurrentAfterFile(file) {
-            this.currentAfterFile = file;
-            this.currentAfterFileId = file.id;
-        },
         storeBlock() {
             let cmpInfo = false;
             let cmpInfoBefore = this.$gettext('Bitte wählen Sie ein Vorherbild aus.');
@@ -269,6 +254,18 @@ export default {
             }
         },
     },
+    watch: {
+        currentBeforeFileId(newId) {
+            if (newId) {
+                this.currentBeforeFile = this.fileRefById({ id: newId });
+            }
+        },
+        currentAfterFileId(newId) {
+            if (newId) {
+                this.currentAfterFile = this.fileRefById({ id: newId });
+            }
+        }
+    }
 };
 </script>
 <style scoped lang="scss">
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
index d26446825d9ddf4b5ed8f34eeb2d25df57c9cf7a..669a5b8ade0df0b8b96ab8cb697384356162da54 100644
--- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -124,10 +124,13 @@
                             </label>
                             <label v-if="currentImage === 'true'">
                                 {{ $gettext('Bilddatei') }}
-                                <courseware-file-chooser
+                                <studip-file-chooser
                                     v-model="currentFileId"
+                                    selectable="file"
+                                    :courseId="studipContext.id"
+                                    :userId="userId"
                                     :isImage="true"
-                                    @selectFile="updateCurrentFile"
+                                    :excludedCourseFolderTypes="excludedCourseFolderTypes"
                                 />
                             </label>
                         </form>
@@ -218,10 +221,11 @@ export default {
     },
     computed: {
         ...mapGetters({
-            userId: 'userId',
+            studipContext: 'context',
+            fileRefById: 'file-refs/byId',
             getUserDataById: 'courseware-user-data-fields/byId',
+            relatedUserData: 'user-data-field/related',
             usersById: 'users/byId',
-            relatedUserData: 'user-data-field/related'
         }),
         userData() {
             return this.getUserDataById({ id: this.block.relationships['user-data-field'].data.id });
@@ -366,11 +370,6 @@ export default {
                 this.buildCanvas();
             });
         },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-            this.buildCanvas();
-        },
         setColor(color) {
             if (this.write) {
                 return;
@@ -700,6 +699,12 @@ export default {
     watch: {
         currentUserView() {
             this.redraw();
+        },
+        currentFileId(newId) {
+            if (newId) {
+                this.currentFile = this.fileRefById({ id: newId });
+                this.buildCanvas();
+            }
         }
     },
 };
diff --git a/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
index 040d27f9b5e73f2d361c466f4e0dd61c49fc5e8e..60d9ae154c46d9a531a8f9a69b7d3af0bed7b2d8 100644
--- a/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareConfirmBlock.vue
@@ -56,7 +56,6 @@ export default {
     },
     computed: {
         ...mapGetters({
-            userId: 'userId',
             getUserDataById: 'courseware-user-data-fields/byId',
         }),
         text() {
diff --git a/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
index 5bfc2f78c3124624de3e84972e953339455e92b4..1847c6d86f5f026410f10a23e017467b99d3ba9f 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDialogCardsBlock.vue
@@ -70,29 +70,35 @@
                     >
                         <form class="default" @submit.prevent="">
                             <label>
-                                {{ $gettext('Bild Vorderseite') }}:
-                                <courseware-file-chooser
+                                {{ $gettext('Bild Vorderseite') }}
+                                <studip-file-chooser
                                     v-model="card.front_file_id"
+                                    selectable="file"
+                                    :courseId="context.id"
+                                    :userId="userId"
                                     :isImage="true"
-                                    :canBeEmpty="true"
-                                    @selectFile="updateFile(index, 'front', $event)"
+                                    :excludedCourseFolderTypes="excludedCourseFolderTypes"
+                                    @select="updateFile(index, 'front', $event)"
                                 />
-                            </label>
+                             </label>
                             <label>
-                                {{ $gettext('Text Vorderseite') }}:
+                                {{ $gettext('Text Vorderseite') }}
                                 <input type="text" v-model="card.front_text" />
                             </label>
                             <label>
-                                {{ $gettext('Bild Rückseite') }}:
-                                <courseware-file-chooser
+                                {{ $gettext('Bild Rückseite') }}
+                                <studip-file-chooser
                                     v-model="card.back_file_id"
+                                    selectable="file"
+                                    :courseId="context.id"
+                                    :userId="userId"
                                     :isImage="true"
-                                    :canBeEmpty="true"
-                                    @selectFile="updateFile(index, 'back', $event)"
+                                    :excludedCourseFolderTypes="excludedCourseFolderTypes"
+                                    @select="updateFile(index, 'back', $event)"
                                 />
                             </label>
                             <label>
-                                {{ $gettext('Text Rückseite') }}:
+                                {{ $gettext('Text Rückseite') }}
                                 <input type="text" v-model="card.back_text" />
                             </label>
                             <label v-if="!onlyCard">
@@ -114,7 +120,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-dialog-cards-block',
@@ -132,6 +138,9 @@ export default {
         };
     },
     computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+        }),
         cards() {
             return this.block?.attributes?.payload?.cards;
         },
@@ -191,20 +200,22 @@ export default {
                 containerId: this.block.relationships.container.data.id,
             });
         },
-        updateFile(cardIndex, side, file) {
+        updateFile(cardIndex, side, fileId) {
             if (side === 'front') {
-                if (file) {
-                    this.currentCards[cardIndex].front_file_id = file.id;
-                    this.currentCards[cardIndex].front_file = file;
+                if (fileId) {
+                    this.currentCards[cardIndex].front_file_id = fileId;
+                    this.currentCards[cardIndex].front_file = this.fileRefById({ id: fileId });
+                    this.currentCards[cardIndex].front_file.download_url = this.currentCards[cardIndex].front_file.meta['download-url'];
                 } else {
                     this.currentCards[cardIndex].front_file_id = '';
                     this.currentCards[cardIndex].front_file = [];
                 }
             }
             if (side === 'back') {
-                if (file) {
-                    this.currentCards[cardIndex].back_file_id = file.id;
-                    this.currentCards[cardIndex].back_file = file;
+                if (fileId) {
+                    this.currentCards[cardIndex].back_file_id = fileId;
+                    this.currentCards[cardIndex].back_file = this.fileRefById({ id: fileId });
+                    this.currentCards[cardIndex].back_file.download_url = this.currentCards[cardIndex].back_file.meta['download-url'];
                 } else {
                     this.currentCards[cardIndex].back_file_id = '';
                     this.currentCards[cardIndex].back_file = [];
diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
index 0af177e27c52cca228a5898c30d2113070133b23..72cfeb3ed480f94fd9ddb1d5dd238c3dd456337e 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
@@ -221,10 +221,13 @@
                     </label>
                     <label>
                         {{ $gettext('Datei') }}
-                        <courseware-file-chooser
+                        <studip-file-chooser
                             v-model="currentFileId"
+                            selectable="file"
+                            :courseId="context.id"
+                            :userId="userId"
                             :isDocument="true"
-                            @selectFile="updateCurrentFile"
+                            :excludedCourseFolderTypes="excludedCourseFolderTypes"
                         />
                     </label>
                     <label>
@@ -269,7 +272,7 @@ import {
 import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
 import { dragscroll } from 'vue-dragscroll';
 
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
 import 'pdfjs-dist/web/pdf_viewer.css';
 
 export default {
@@ -327,6 +330,9 @@ export default {
         };
     },
     computed: {
+        ...mapGetters({
+            fileRefById: 'file-refs/byId',
+        }),
         title() {
             return this.block?.attributes?.payload?.title;
         },
@@ -369,6 +375,11 @@ export default {
         showPdfSearchBox() {
             this.resetPdfSearch();
         },
+        currentFileId(newId) {
+            if (newId) {
+                this.currentFile = this.fileRefById({ id: newId });
+            }
+        }
     },
     mounted() {
         if (this.block.id) {
@@ -392,10 +403,6 @@ export default {
             this.currentFileId = this.fileId;
             this.currentDocType = this.docType;
         },
-        updateCurrentFile(file) {
-            this.currentFile = file;
-            this.currentFileId = file.id;
-        },
         initPdfTask() {
             if (this.currentUrl) {
                 let view = this;
diff --git a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
index 2897b085568befd5da8baa6b17cfc323cddc0f68..ff01d78a2638252e0206f149c6ba250880f4c48c 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDownloadBlock.vue
@@ -51,7 +51,7 @@
                             </label>
                             <label>
                                 {{ $gettext('Datei') }}
-                                <courseware-file-chooser v-model="currentFileId" @selectFile="updateCurrentFile" />
+                                <studip-file-chooser v-model="currentFileId" selectable="file" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" />
                             </label>
                         </form>
                     </courseware-tab>
diff --git a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
index 6bbc48d00a8455edd368a47c5ecac5ff10ea69d4..0eefa6cc040e727fe86f0cecd073d0ed29121478 100644
--- a/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareFolderBlock.vue
@@ -140,7 +140,7 @@
                     </label>
                     <label>
                         {{ $gettext('Ordner') }}
-                        <courseware-folder-chooser v-model="currentFolderId" allowUserFolders allowHomeworkFolders />
+                        <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" />
                     </label>
                 </form>
             </template>
diff --git a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
index 40bc33c5dad2cc1030055226053856b2aef91793..816447edda216c0004efc446ca94ea1462de0c14 100644
--- a/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareGalleryBlock.vue
@@ -86,7 +86,7 @@
                             </label>
                             <label>
                                 {{ $gettext('Ordner') }}
-                                <courseware-folder-chooser v-model="currentFolderId" allowUserFolders />
+                                <studip-file-chooser v-model="currentFolderId" selectable="folder" :courseId="context.id" :userId="userId" :excludedCourseFolderTypes="excludedCourseFolderTypes" />
                             </label>
                             <label v-if="currentLayout === 'carousel'">
                                 {{ $gettext('Höhe') }}
diff --git a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
index c7dd6b52ab426e2f0290b9903f3a5ea4a78728f1..c9c2ce0ac7fa0f66b94040efaf471944d5b657ee 100644
--- a/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareHeadlineBlock.vue
@@ -242,11 +242,15 @@
                                 </template>
 
                                 <template v-if="!currentBackgroundImageId">
-                                    <courseware-file-chooser
+                                    <studip-file-chooser
                                         v-model="currentBackgroundImageId"
+                                        selectable="file"
+                                        :courseId="context.id"
+                                        :userId="userId"
                                         :isImage="true"
-                                        @selectFile="onSelectFile"
-                                        />
+                                        :excludedCourseFolderTypes="excludedCourseFolderTypes"
+                                        @select="onSelectFile"
+                                    />
                                     <div style="margin-block-start: 1em">{{ $gettext('oder') }}</div>
                                     <button class="button" type="button" @click="showStockImageSelector = true">
                                         {{ $gettext('Aus dem Bilderpool auswählen') }}
@@ -340,11 +344,11 @@ export default {
     },
     computed: {
         ...mapGetters({
+            currentStructuralElementImageURL: 'currentStructuralElementImageURL',
             fileRefById: 'file-refs/byId',
+            relatedTermOfUse: 'terms-of-use/related',
             stockImageById: 'stock-images/byId',
             urlHelper: 'urlHelper',
-            relatedTermOfUse: 'terms-of-use/related',
-            currentStructuralElementImageURL: 'currentStructuralElementImageURL'
         }),
         title() {
             return this.block?.attributes?.payload?.title;
@@ -531,8 +535,10 @@ export default {
             this.currentBackgroundImageType = file.type;
             this.currentBackgroundURL = file.download_url;
         },
-        onSelectFile(file) {
-            this.updateCurrentBackgroundImage({ ...file, type: 'file-refs' });
+        onSelectFile(fileId) {
+            let file = this.fileRefById({ id: fileId });
+            file.download_url = file.meta['download-url'];
+            this.updateCurrentBackgroundImage(file);
         },
         storeText() {
             let attributes = {};
diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
index 90df3f66e0970660bbcc3f4b50ba36b7da804ab3..d628b12e78c24e99b855d64d6694e2b18643b7b0 100644
--- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
@@ -123,7 +123,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions } from 'vuex';
 import md5 from 'md5';
 
 export default {
@@ -150,7 +150,6 @@ export default {
         };
     },
     computed: {
-        ...mapGetters(['userId']),
         url() {
             return this.block?.attributes?.payload?.url;
         },
diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
index 41b39d2cb395d2f27139e81f785a7b3b8191903e..6d129801ef5da65ccea2ae89b96845014f90fb43 100644
--- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
@@ -78,10 +78,13 @@
                 <form class="default" @submit.prevent="">
                     <label>
                         {{ $gettext('Bilddatei') }}
-                        <courseware-file-chooser
+                        <studip-file-chooser
                             v-model="currentFileId"
+                            selectable="file"
+                            :courseId="studipContext.id"
+                            :userId="userId"
                             :isImage="true"
-                            @selectFile="updateCurrentFile"
+                            :excludedCourseFolderTypes="excludedCourseFolderTypes"
                         />
                     </label>
                     <label>
@@ -307,6 +310,7 @@ export default {
     },
     computed: {
         ...mapGetters({
+            studipContext: 'context',
             courseware: 'courseware-structural-elements/all',
             fileRefById: 'file-refs/byId',
             urlHelper: 'urlHelper',
@@ -343,6 +347,9 @@ export default {
         },
         async loadFile() {
             const id = this.currentFileId;
+            if (id === '') {
+                return;
+            }
             await this.loadFileRef({ id });
             const fileRef = this.fileRefById({ id });
 
@@ -797,6 +804,13 @@ export default {
             }
         },
     },
+    watch: {
+        currentFileId(newId) {
+            if (newId) {
+                this.loadFile();
+            }
+        }
+    }
 };
 </script>
 <style scoped lang="scss">
diff --git a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
index dd619dd1d8c2e03d64278fcb5c99d0b311c3844c..99d42647df1c77f0d63edd662671d49103cda175 100644
--- a/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareTableOfContentsBlock.vue
@@ -100,7 +100,6 @@ export default {
         ...mapGetters({
             childrenById: 'courseware-structure/children',
             structuralElementById: 'courseware-structural-elements/byId',
-            context: 'context',
             taskById: 'courseware-tasks/byId',
             userById: 'users/byId',
             groupById: 'status-groups/byId',
diff --git a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
index 2ed3074426222f17d14b4a7a9abcf9f83f4ea761..7febfbc0a6160c3321c73c0eb79b76aee2496846 100644
--- a/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareVideoBlock.vue
@@ -45,10 +45,13 @@
                             </label>
                             <label v-show="currentSource === 'studip'">
                                 {{ $gettext('Datei') }}
-                                <courseware-file-chooser
+                                <studip-file-chooser
                                     v-model="currentFileId"
+                                    selectable="file"
+                                    :courseId="context.id"
+                                    :userId="userId"
                                     :isVideo="true"
-                                    @selectFile="updateCurrentFile"
+                                    :excludedCourseFolderTypes="excludedCourseFolderTypes"
                                 />
                             </label>
 
@@ -230,6 +233,13 @@ export default {
             }
         },
     },
+    watch: {
+        currentFileId(newId) {
+            if (newId) {
+                this.currentFile = this.fileRefById({ id: newId });
+            }
+        }
+    }
 };
 </script>
 <style scoped lang="scss">
diff --git a/resources/vue/components/courseware/blocks/block-components.js b/resources/vue/components/courseware/blocks/block-components.js
index a79ed4a30493de0632b116253bab80a8c2969b5f..b0950595bd1cd7a9e447edd7d61e57315149d701 100644
--- a/resources/vue/components/courseware/blocks/block-components.js
+++ b/resources/vue/components/courseware/blocks/block-components.js
@@ -5,6 +5,8 @@ import CoursewareTabs from '../layouts/CoursewareTabs.vue';
 import CoursewareTab from '../layouts/CoursewareTab.vue';
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'
 
+import StudipFileChooser from './../../StudipFileChooser.vue';
+
 const BlockComponents = {
     CoursewareDefaultBlock,
     CoursewareFileChooser,
@@ -13,6 +15,8 @@ const BlockComponents = {
     CoursewareTabs,
     CoursewareTab,
     CoursewareCompanionBox,
+
+    StudipFileChooser,
 }
 
 export default BlockComponents;
\ No newline at end of file
diff --git a/resources/vue/components/file-chooser/FileChooserBox.vue b/resources/vue/components/file-chooser/FileChooserBox.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cc5a1bd7d2cb6f2bed578d04478e756195502297
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserBox.vue
@@ -0,0 +1,249 @@
+<template>
+    <div class="file-chooser-box">
+        <header>
+            <file-chooser-breadcrumb :folders="folders" />
+            <button class="toggle-view" :title="$gettext('Rasteransicht umschalten')" @click="toggleGrid">
+                <studip-icon :shape="showGrid ? 'view-list' : 'view-wall'" />
+            </button>
+        </header>
+        <div v-if="showMessageBox" class="messagebox messagebox_success">
+            <div class="messagebox_buttons">
+                <a
+                    href="#"
+                    :title="$gettext('Nachrichtenbox schließen')"
+                    class="close"
+                    @click.prevent="showMessageBox = false"
+                ></a>
+            </div>
+            {{ successMessage }}
+        </div>
+        <div v-if="contentForbidden" class="messagebox messagebox_error">
+            {{ $gettext('Sie sind nicht berechtigt, den Inhalt dieses Ordners anzuzeigen.') }}
+        </div>
+        <div v-else class="file-chooser-box-content">
+            <div v-if="showGrid" class="file-chooser-items">
+                <template v-if="!isEmpty">
+                    <file-chooser-folder-item v-for="folder in subFolders" :key="folder.id" :folder="folder" />
+                    <file-chooser-file-item
+                        v-for="file in currentFolderFiles"
+                        :key="file.id"
+                        :file="file"
+                        @selectId="$emit('selectId')"
+                    />
+                    <studip-progress-indicator v-if="loadingFiles" :description="$gettext('Lade Dateien…')" />
+                </template>
+                <file-chooser-empty v-if="isEmpty" />
+            </div>
+            <file-chooser-table
+                v-else
+                :files="currentFolderFiles"
+                :subfolders="subFolders"
+                :empty="isEmpty"
+                @selectId="$emit('selectId')"
+            />
+            <file-chooser-toolbar
+                :class="{ 'with-table': !showGrid }"
+                @fileAdded="fileAdded"
+                @folderAdded="folderAdded"
+            />
+        </div>
+    </div>
+</template>
+
+<script>
+import FileChooserBreadcrumb from './FileChooserBreadcrumb.vue';
+import FileChooserEmpty from './FileChooserEmpty.vue';
+import FileChooserFileItem from './FileChooserFileItem.vue';
+import FileChooserFolderItem from './FileChooserFolderItem.vue';
+import FileChooserTable from './FileChooserTable.vue';
+import FileChooserToolbar from './FileChooserToolbar.vue';
+import StudipProgressIndicator from '../StudipProgressIndicator.vue';
+
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'file-chooser-box',
+    components: {
+        FileChooserBreadcrumb,
+        FileChooserEmpty,
+        FileChooserFileItem,
+        FileChooserFolderItem,
+        FileChooserTable,
+        FileChooserToolbar,
+        StudipProgressIndicator,
+    },
+    props: {
+        excludedFolderTypes: { type: Array, default: () => [] },
+    },
+    data() {
+        return {
+            loadingFiles: false,
+            showGrid: true,
+            showMessageBox: false,
+            successMessage: '',
+            contentForbidden: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            activeFolder: 'file-chooser/activeFolder',
+            activeFolderId: 'file-chooser/activeFolderId',
+            currentFolderFiles: 'file-chooser/currentFolderFiles',
+            relatedUsersFolders: 'file-chooser/relatedUsersFolders',
+            relatedCoursesFolders: 'file-chooser/relatedCoursesFolders',
+            rangeType: 'file-chooser/activeFolderRangeType',
+        }),
+        rootFolder() {
+            if (this.folders.length > 0) {
+                return this.folders.find((folder) => {
+                    return folder.attributes['folder-type'] === 'RootFolder';
+                });
+            }
+            return null;
+        },
+        folders() {
+            if (this.rangeType === 'courses') {
+                return this.relatedCoursesFolders;
+            }
+            if (this.rangeType === 'users') {
+                return this.relatedUsersFolders;
+            }
+
+            return [];
+        },
+        subFolders() {
+            const excludedFolderTypes = ['InboxFolder', 'OutboxFolder'].concat(this.excludedFolderTypes);
+            return this.folders.filter((folder) => {
+                return (
+                    folder.relationships?.parent?.data?.id === this.activeFolderId
+                    && !excludedFolderTypes.includes(folder.attributes['folder-type'])
+                );
+            });
+        },
+        isEmpty() {
+            return this.activeFolder?.attributes['is-empty'] ?? false;
+        },
+        foldersCounter() {
+            return this.activeFolder?.relationships?.folders?.meta?.count ?? 0;
+        },
+        filesCounter() {
+            return this.activeFolder?.relationships?.['file-refs']?.meta?.count ?? 0;
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadFolderFiles: 'file-chooser/loadFolderFiles',
+        }),
+        async loadFiles(folderId) {
+            this.contentForbidden = false;
+            if (this.filesCounter === 0) {
+                return;
+            }
+            setTimeout(() => {
+                if (loading) {
+                    this.loadingFiles = true;
+                }
+            }, 100);
+            let loading = true;
+            try {
+                await this.loadFolderFiles({ folderId });
+            } catch(response) {
+                if (response.data?.errors[0].status === '403') {
+                    this.contentForbidden = true;
+                }
+            }
+            loading = false;
+            this.loadingFiles = false;
+        },
+        toggleGrid() {
+            this.showGrid = !this.showGrid;
+        },
+        fileAdded() {
+            this.showMessageBox = true;
+            this.successMessage = this.$gettext('Es wurde eine Datei hochgeladen.');
+        },
+        folderAdded() {
+            this.showMessageBox = true;
+            this.successMessage = this.$gettext('Der Ordner wurde angelegt.');
+        },
+    },
+    watch: {
+        activeFolderId(newId) {
+            this.showMessageBox = false;
+            this.loadFiles(newId);
+        },
+    },
+};
+</script>
+<style lang="scss">
+.file-chooser-box {
+    flex-grow: 1;
+
+    header {
+        display: flex;
+        flex-direction: row;
+        position: sticky;
+        top: 0;
+        background-color: var(--content-color-20);
+        padding: 0.5em 1em;
+        border: solid thin var(--content-color-40);
+        margin-bottom: 1em;
+
+        .file-chooser-breadcrumb {
+            flex-grow: 1;
+        }
+        .toggle-view {
+            width: 20px;
+            height: 20px;
+            border: none;
+            background-color: transparent;
+            cursor: pointer;
+        }
+    }
+    .file-chooser-box-content {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        overflow-y: scroll;
+        height: calc(100% - 36px);
+    }
+}
+.file-chooser-items {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    overflow-y: auto;
+
+    .file-chooser-item {
+        display: flex;
+        flex-direction: column;
+        width: 104px;
+        min-height: 104px;
+        border: solid thin transparent;
+        background-color: transparent;
+        word-break: break-word;
+        margin: 0 4px 4px 4px;
+        padding: 4px;
+        cursor: pointer;
+
+        &.selected {
+            background-color: var(--activity-color-20);
+            border: solid thin var(--base-color);
+            font-weight: 700;
+        }
+        &.disabled {
+            cursor: default;
+        }
+        img {
+            margin: 0 auto;
+        }
+        span {
+            width: 100%;
+            overflow: hidden;
+            display: -webkit-box;
+            -webkit-line-clamp: 2;
+            -webkit-box-orient: vertical;
+        }
+    }
+}
+</style>
diff --git a/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5f0ffa1a1d83049e3770b3d4314ac8ba7f2632c1
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserBreadcrumb.vue
@@ -0,0 +1,87 @@
+<template>
+    <ol class="file-chooser-breadcrumb">
+        <li v-for="(folder, index) in breadcrumbItems" :key="folder.id">
+            <a href="#" @click.prevent="selectFolder(folder)">
+                <template v-if="rootId !== folder.id">
+                    {{ folder.attributes.name }}
+                </template>
+                <studip-icon v-else shape="home" :title="homeTitle"/>
+            </a>
+            <span v-if="breadcrumbItems.length > 1 && index !== breadcrumbItems.length - 1">/</span>
+        </li>
+    </ol>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+export default {
+    name: 'file-chooser-breadcrumb',
+    props: {
+        folders: Array,
+    },
+    data() {
+        return {
+            breadcrumbItems: [],
+            rootId: ''
+        };
+    },
+    computed: {
+        ...mapGetters({
+            activeFolderId: 'file-chooser/activeFolderId',
+            activeFolder: 'file-chooser/activeFolder',
+            folderById: 'folders/byId',
+        }),
+        homeTitle() {
+            return this.activeFolderRangeType === 'users' ? this.$gettext('Arbeitsplatz') : this.$gettext('Diese Veranstaltung');
+        }
+    },
+    methods: {
+        ...mapActions({
+            setActiveFolderId: 'file-chooser/setActiveFolderId',
+            setSelectedFolderId: 'file-chooser/setSelectedFolderId'
+        }),
+        selectFolder(folder) {
+            this.setActiveFolderId(folder.id);
+        },
+        updateBreadcrumb() {
+            this.breadcrumbItems = [];
+            this.addBreadcrumbItem(this.activeFolder);
+            this.breadcrumbItems = this.breadcrumbItems.reverse();
+        },
+        addBreadcrumbItem(folder) {
+            this.breadcrumbItems.push(folder);
+            if (folder.relationships.parent) {
+                const id = folder.relationships.parent.data.id;
+                const parent = this.folderById({ id });
+                this.addBreadcrumbItem(parent);
+            } else {
+                this.rootId = folder.id;
+            }
+        },
+    },
+    watch: {
+        activeFolderId(newId) {
+            this.updateBreadcrumb();
+            this.setSelectedFolderId('');
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.file-chooser-breadcrumb {
+    display: flex;
+    flex-direction: row;
+    padding: 0;
+    margin: 0;
+    li {
+        list-style: none;
+        a img {
+            vertical-align: text-bottom;
+        }
+        span {
+            padding: 0 4px 0 0;
+        }
+    }
+}
+</style>
diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2102d1e3b0d416f4c53c676f54c51e6ff3f1722f
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserDialog.vue
@@ -0,0 +1,295 @@
+<template>
+    <studip-dialog
+        :title="dialogTitle"
+        :confirmText="$gettext('Auswählen')"
+        confirmClass="accept"
+        :closeText="$gettext('Abbrechen')"
+        closeClass="cancel"
+        @close="$emit('close')"
+        @confirm="selectId"
+        :confirmDisabled="!hasSelection"
+        :height="height"
+        :width="width"
+    >
+        <template v-slot:dialogContent>
+            <div class="file-chooser-content">
+                <ul class="file-chooser-folder-selector">
+                    <li v-if="courseId && allowCourseFolders" class="file-chooser-tree-item">
+                        <a
+                            href="#"
+                            @click.prevent="setFolder('courses')"
+                            :class="{ selected: coursesRootFolderSelected }"
+                        >
+                            <studip-icon shape="seminar" />
+                            <span>{{ $gettext('Diese Veranstaltung') }}</span>
+                        </a>
+                        <ul class="file-chooser-tree file-chooser-tree-first-level">
+                            <file-chooser-tree v-for="child in coursesTree.children" :key="child.id" :folder="child" />
+                        </ul>
+                    </li>
+                    <li v-if="userId && allowUserFolders" class="file-chooser-tree-item">
+                        <a href="#" @click.prevent="setFolder('users')" :class="{ selected: usersRootFolderSelected }">
+                            <studip-icon shape="content" />
+                            <span>{{ $gettext('Arbeitsplatz') }}</span>
+                        </a>
+                        <ul class="file-chooser-tree file-chooser-tree-first-level">
+                            <file-chooser-tree v-for="child in usersTree.children" :key="child.id" :folder="child" />
+                        </ul>
+                    </li>
+                </ul>
+                <file-chooser-box
+                    :excludedFolderTypes="[...excludedCourseFolderTypes, ...excludedUserFolderTypes]"
+                    @selectId="selectId"
+                />
+            </div>
+        </template>
+    </studip-dialog>
+</template>
+<script>
+import FileChooserBox from './FileChooserBox.vue';
+import FileChooserTree from './FileChooserTree.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'file-chooser-dialog',
+    components: {
+        FileChooserBox,
+        FileChooserTree,
+    },
+    props: {
+        selectable: {
+            type: String,
+            default: 'file',
+            validator: (value) => {
+                return ['file', 'folder'].includes(value);
+            },
+        },
+        selectedId: {
+            type: String,
+            required: false,
+        },
+        courseId: {
+            type: String,
+            validator: (value) => {
+                return value !== '';
+            },
+            required: false,
+        },
+        userId: {
+            type: String,
+            validator: (value) => {
+                return value !== '';
+            },
+            required: false,
+        },
+        isImage: { type: Boolean, default: false },
+        isVideo: { type: Boolean, default: false },
+        isAudio: { type: Boolean, default: false },
+        isDocument: { type: Boolean, default: false },
+        excludedCourseFolderTypes: { type: Array, default: () => [] },
+        excludedUserFolderTypes: { type: Array, default: () => [] },
+        allowUserFolders: { type: Boolean, default: true },
+        allowCourseFolders: { type: Boolean, default: true },
+    },
+    data() {
+        return {
+            height: 600,
+            width: 1000,
+            scope: 'courses',
+            coursesTree: [],
+            usersTree: [],
+        };
+    },
+    computed: {
+        ...mapGetters({
+            activeFolderId: 'file-chooser/activeFolderId',
+            relatedUsersFolders: 'file-chooser/relatedUsersFolders',
+            relatedCoursesFolders: 'file-chooser/relatedCoursesFolders',
+            isFolderChooser: 'file-chooser/isFolderChooser',
+            selectedFileId: 'file-chooser/selectedFileId',
+            selectedFolderId: 'file-chooser/selectedFolderId',
+            fileById: 'file-refs/byId',
+            folderById: 'folders/byId',
+        }),
+        dialogTitle() {
+            if (this.isFolderChooser) {
+                return this.$gettext('Ordner auswählen');
+            }
+
+            return this.$gettext('Datei auswählen');
+        },
+        showCourse() {
+            return this.scope === 'courses';
+        },
+        showUser() {
+            return this.scope === 'users';
+        },
+        hasSelection() {
+            if (this.isFolderChooser) {
+                return this.selectedFolderId !== '';
+            }
+
+            return this.selectedFileId !== '';
+        },
+        coursesRootFolder() {
+            return this.getRootFolder(this.relatedCoursesFolders);
+        },
+        usersRootFolder() {
+            return this.getRootFolder(this.relatedUsersFolders);
+        },
+        coursesRootFolderSelected() {
+            return this.coursesRootFolder?.id === this.activeFolderId;
+        },
+        usersRootFolderSelected() {
+            return this.usersRootFolder?.id === this.activeFolderId;
+        },
+    },
+    methods: {
+        ...mapActions({
+            setCourseId: 'file-chooser/setCourseId',
+            setUserId: 'file-chooser/setUserId',
+            setSelectable: 'file-chooser/setSelectable',
+            setIsAudio: 'file-chooser/setIsAudio',
+            setIsDocument: 'file-chooser/setIsDocument',
+            setIsImage: 'file-chooser/setIsImage',
+            setIsVideo: 'file-chooser/setIsVideo',
+
+            setSelectedFileId: 'file-chooser/setSelectedFileId',
+            setSelectedFolderId: 'file-chooser/setSelectedFolderId',
+            setActiveFolderId: 'file-chooser/setActiveFolderId',
+            loadRangeFolders: 'file-chooser/loadRangeFolders',
+        }),
+        selectId() {
+            if (this.isFolderChooser) {
+                this.$emit('selected', this.selectedFolderId);
+            } else {
+                this.$emit('selected', this.selectedFileId);
+            }
+        },
+        setDimensions() {
+            this.height = (window.innerHeight * 0.8).toFixed(0);
+            this.width = Math.min((window.innerWidth * 0.9).toFixed(0), 1200).toFixed(0);
+        },
+        setFolder(range) {
+            if (range === 'courses') {
+                this.setActiveFolderId(this.coursesRootFolder.id);
+            }
+            if (range === 'users') {
+                this.setActiveFolderId(this.usersRootFolder.id);
+            }
+        },
+        getRootFolder(folders) {
+            if (folders?.length > 0) {
+                return folders.filter((folder) => {
+                    return folder.attributes['folder-type'] === 'RootFolder';
+                })[0];
+            }
+            return null;
+        },
+        getFolderTree(rootFolder, folders, excludedFolderTypes) {
+            if (rootFolder) {
+                rootFolder.children = this.getSubfolders(rootFolder, folders, excludedFolderTypes);
+
+                return rootFolder;
+            }
+
+            return [];
+        },
+        getSubfolders(parent, folders, excludedFolderTypes) {
+            const children = folders.filter((folder) => {
+                return (
+                    folder.relationships?.parent?.data?.id === parent.id &&
+                    !excludedFolderTypes.includes(folder.attributes['folder-type'])
+                );
+            });
+            children.forEach((child) => {
+                child.children = this.getSubfolders(child, folders, excludedFolderTypes);
+            });
+
+            return children;
+        },
+    },
+    async mounted() {
+        this.setDimensions();
+
+        this.setSelectable(this.selectable);
+        this.setCourseId(this.courseId);
+        this.setUserId(this.userId);
+        if (this.selectable === 'file') {
+            this.setIsAudio(this.isAudio);
+            this.setIsDocument(this.isDocument);
+            this.setIsImage(this.isImage);
+            this.setIsVideo(this.isVideo);
+        }
+
+        if (this.userId && this.allowUserFolders) {
+            await this.loadRangeFolders({ rangeType: 'users', rangeId: this.userId });
+            const excludedFolderTypes = ['InboxFolder', 'OutboxFolder'];
+            this.usersTree = this.getFolderTree(this.usersRootFolder, this.relatedUsersFolders, [
+                ...excludedFolderTypes,
+                ...this.excludedUserFolderTypes,
+            ]);
+            if (!this.courseId && !this.selectedId) {
+                this.setActiveFolderId(this.usersRootFolder.id);
+            }
+        }
+
+        if (this.courseId && this.allowCourseFolders) {
+            await this.loadRangeFolders({ rangeType: 'courses', rangeId: this.courseId });
+            this.coursesTree = this.getFolderTree(
+                this.coursesRootFolder,
+                this.relatedCoursesFolders,
+                this.excludedCourseFolderTypes
+            );
+            if (!this.selectedId) {
+                this.setActiveFolderId(this.coursesRootFolder.id);
+            }
+        }
+
+        if (this.selectedId) {
+            if (this.isFolderChooser) {
+                const folder = this.folderById({ id: this.selectedId });
+                this.setActiveFolderId(folder.relationships.parent.data.id);
+                this.$nextTick(() => {
+                    this.setSelectedFolderId(this.selectedId);
+                });
+            } else {
+                const file = this.fileById({ id: this.selectedId });
+                this.setActiveFolderId(file.relationships.parent.data.id);
+                this.setSelectedFileId(file.id);
+            }
+        }
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.file-chooser-content {
+    display: flex;
+    flex-direction: row;
+    height: 100%;
+    .file-chooser-folder-selector {
+        min-width: 270px;
+        max-width: 270px;
+        list-style: none;
+        margin: 0 1em 0 0;
+        padding: 0 1em 0 0;
+        border-right: solid thin var(--content-color-40);
+        overflow-y: auto;
+    }
+}
+
+
+@media (max-width: 580px) {
+    .file-chooser-content .file-chooser-folder-selector {
+        display: none;
+    }
+}
+@media (max-width: 768px) {
+    .file-chooser-content .file-chooser-folder-selector {
+        min-width: 130px;
+        max-width: 130px;
+    }
+}
+
+</style>
diff --git a/resources/vue/components/file-chooser/FileChooserEmpty.vue b/resources/vue/components/file-chooser/FileChooserEmpty.vue
new file mode 100644
index 0000000000000000000000000000000000000000..fcadae0199f88571352c362e395d39faa91feecf
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserEmpty.vue
@@ -0,0 +1,19 @@
+<template>
+    <div class="file-chooser-empty">
+        <studip-icon shape="folder-empty" :size="128" role="inactive" />
+        {{ $gettext('Dieser Ordner ist leer') }}
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'file-chooser-empty',
+};
+</script>
+<style lang="scss">
+.file-chooser-empty {
+    display: flex;
+    flex-direction: column;
+    margin: 1em auto;
+}
+</style>
diff --git a/resources/vue/components/file-chooser/FileChooserFileItem.vue b/resources/vue/components/file-chooser/FileChooserFileItem.vue
new file mode 100644
index 0000000000000000000000000000000000000000..90c2f397d46e432c4640ce1d65b5324732a0f04f
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserFileItem.vue
@@ -0,0 +1,142 @@
+<template v-if="isReadable">
+    <button
+        v-if="isButton"
+        class="file-chooser-item"
+        :class="{ selected: isSelected, disabled: isFolderChooser }"
+        :title="fileName"
+        @click="selectFile"
+        v-on:dblclick="instantSelectFile"
+    >
+        <studip-icon :shape="fileIcon" :size="48" />
+        <span>{{ fileName }}</span>
+    </button>
+    <tr v-else :class="{ selected: isSelected }">
+        <td class="document-icon">
+            <a href="#" @click.prevent="selectFile" v-on:dblclick.prevent="instantSelectFile">
+                <studip-icon :shape="fileIcon" :size="24" />
+            </a>
+        </td>
+        <td>
+            <a href="#" @click.prevent="selectFile" v-on:dblclick.prevent="instantSelectFile">{{ fileName }}</a>
+        </td>
+        <td>{{ fileSize }}</td>
+        <td class="responsive-hidden">{{ fileOwner }}</td>
+        <td>{{ fileMkdate }}</td>
+    </tr>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+export default {
+    name: 'file-chooser-file-item',
+    props: {
+        file: {
+            type: Object,
+            required: true,
+        },
+        tag: {
+            type: String,
+            default: 'button',
+            validator: (tag) => {
+                return ['button', 'tr'].includes(tag);
+            },
+        },
+    },
+    computed: {
+        ...mapGetters({
+            selectedFileId: 'file-chooser/selectedFileId',
+            isFolderChooser: 'file-chooser/isFolderChooser',
+        }),
+        isReadable() {
+            return this.file.attributes['is-readable'];
+        },
+        isSelected() {
+            return this.selectedFileId === this.file.id;
+        },
+        isButton() {
+            return this.tag === 'button';
+        },
+        isTableRow() {
+            return this.tag === 'tr';
+        },
+        fileName() {
+            return this.file.attributes.name;
+        },
+        fileMimeType() {
+            return this.file.attributes['mime-type'];
+        },
+        fileIcon() {
+            if (this.fileMimeType.includes('audio')) {
+                return 'file-audio2';
+            }
+            if (this.fileMimeType.includes('video')) {
+                return 'file-video';
+            }
+            if (this.fileMimeType.includes('image')) {
+                return 'file-pic2';
+            }
+            if (this.fileMimeType.includes('pdf')) {
+                return 'file-pdf';
+            }
+            if (this.fileMimeType.includes('zip')) {
+                return 'file-archive';
+            }
+            if (this.fileMimeType.includes(['msexcel', 'spreadsheetml.sheet'])) {
+                return 'file-excel';
+            }
+            if (this.fileMimeType.includes(['opendocument.spreadsheet'])) {
+                return 'file-spreadsheet';
+            }
+            if (this.fileMimeType.includes(['msword', 'wordprocessingml.document'])) {
+                return 'file-word';
+            }
+            if (this.fileMimeType.includes(['opendocument.text'])) {
+                return 'file-text';
+            }
+            if (this.fileMimeType.includes(['mspowerpoint', 'presentationml.presentation'])) {
+                return 'file-ppt ';
+            }
+            if (this.fileMimeType.includes(['opendocument.presentation'])) {
+                return 'file-presentation';
+            }
+
+            return 'file';
+        },
+        fileSize() {
+            let i = -1;
+            let size = this.file.attributes.filesize;
+            const units = ['KB', 'MB', 'GB', 'TB'];
+            do {
+                size = size / 1024;
+                i++;
+            } while (size > 1000);
+
+            return Math.max(size, 0.1).toFixed(1) + ' ' + units[i];
+        },
+        fileOwner() {
+            return this.file.relationships.owner?.meta?.name;
+        },
+        fileMkdate() {
+            const date = new Date(this.file.attributes.mkdate);
+            const options = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' };
+
+            return date.toLocaleDateString('de-DE', options);
+        },
+    },
+    methods: {
+        ...mapActions({
+            setSelectedFileId: 'file-chooser/setSelectedFileId',
+        }),
+        selectFile() {
+            if (this.isFolderChooser) {
+                return;
+            }
+            this.setSelectedFileId(this.file.id);
+        },
+        instantSelectFile() {
+            this.selectFile();
+            this.$emit('selectId');
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/file-chooser/FileChooserFolderItem.vue b/resources/vue/components/file-chooser/FileChooserFolderItem.vue
new file mode 100644
index 0000000000000000000000000000000000000000..116f582458637ac511e8141a23466a0f8e05da6f
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserFolderItem.vue
@@ -0,0 +1,102 @@
+<template v-if="folderIsReadable">
+    <button
+        v-if="isButton"
+        class="file-chooser-item"
+        :class="{ selected: isSelected, disabled: !isFolderChooser }"
+        :title="folderName"
+        @click="selectFolder"
+        v-on:dblclick="openFolder"
+    >
+        <studip-icon :shape="folderIcon" :size="48" />
+        <span>{{ folderName }}</span>
+    </button>
+    <tr v-else :class="{ selected: isSelected }">
+        <td class="document-icon">
+            <a href="#" @click.prevent="selectFolder" v-on:dblclick.prevent="openFolder"
+                ><studip-icon :shape="folderIcon" :size="24"
+            /></a>
+        </td>
+        <td>
+            <a href="#" @click.prevent="selectFolder" v-on:dblclick.prevent="openFolder">{{ folderName }}</a>
+        </td>
+        <td>
+            <template v-if="!folderIsEmpty">{{ folderSize }}</template>
+        </td>
+        <td class="responsive-hidden">{{ folderOwner }}</td>
+        <td>{{ folderMkdate }}</td>
+    </tr>
+</template>
+
+<script>
+import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js';
+import { mapActions, mapGetters } from 'vuex';
+export default {
+    name: 'file-chooser-folder-item',
+    mixins: [folderIconMixin],
+    props: {
+        folder: {
+            type: Object,
+            required: true,
+        },
+        tag: {
+            type: String,
+            default: 'button',
+            validator: (tag) => {
+                return ['button', 'tr'].includes(tag);
+            },
+        },
+    },
+    computed: {
+        ...mapGetters({
+            selectedFolderId: 'file-chooser/selectedFolderId',
+            isFolderChooser: 'file-chooser/isFolderChooser',
+        }),
+        isSelected() {
+            return this.selectedFolderId === this.folder.id;
+        },
+        isButton() {
+            return this.tag === 'button';
+        },
+        isTableRow() {
+            return this.tag === 'tr';
+        },
+        folderOwner() {
+            return this.folder.relationships.owner?.meta?.name;
+        },
+        folderMkdate() {
+            const date = new Date(this.folder.attributes.mkdate);
+            const options = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' };
+
+            return date.toLocaleDateString('de-DE', options);
+        },
+        folderSubfolderCounter() {
+            return this.folder.relationships?.folders?.meta?.count ?? 0;
+        },
+        folderFilesCounter() {
+            return this.folder.relationships?.['file-refs']?.meta?.count ?? 0;
+        },
+        folderSize() {
+            const length = this.folderSubfolderCounter + this.folderFilesCounter;
+            return this.$gettextInterpolate(this.$ngettext('%{length} Objekt', '%{length} Objekte', length), {
+                length: length,
+            });
+        },
+    },
+    methods: {
+        ...mapActions({
+            setSelectedFolderId: 'file-chooser/setSelectedFolderId',
+            setActiveFolderId: 'file-chooser/setActiveFolderId',
+        }),
+        selectFolder() {
+            if (!this.isFolderChooser) {
+                return;
+            }
+            this.setSelectedFolderId(this.folder.id);
+        },
+        openFolder() {
+            this.setActiveFolderId(this.folder.id);
+            this.setSelectedFolderId('');
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/file-chooser/FileChooserTable.vue b/resources/vue/components/file-chooser/FileChooserTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ad7363dc9e6d5e5e03d3bd28c39381471daca632
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserTable.vue
@@ -0,0 +1,200 @@
+<template>
+    <table class="default">
+        <colgroup>
+            <col style="width: 36px" />
+            <col />
+            <col style="width: 100px" />
+            <col class="responsive-hidden" style="width: 150px" />
+            <col style="width: 126px" />
+        </colgroup>
+        <thead>
+            <tr class="sortable">
+                <th>{{ $gettext('Typ') }}</th>
+                <th :class="getSortClass('name')" @click="sort('name')">
+                    <a href="#">{{ $gettext('Name') }}</a>
+                </th>
+                <th :class="getSortClass('size')" @click="sort('size')">
+                    <a href="#">{{ $gettext('Größe') }}</a>
+                </th>
+                <th class="responsive-hidden" :class="getSortClass('owner')" @click="sort('owner')">
+                    <a href="#">{{ $gettext('Autor/-in') }}</a>
+                </th>
+                <th :class="getSortClass('mkdate')" @click="sort('mkdate')">
+                    <a href="#">{{ $gettext('Datum') }}</a>
+                </th>
+            </tr>
+        </thead>
+        <tbody v-if="empty">
+            <tr class="empty">
+                <td colspan="5">{{ $gettext('Dieser Ordner ist leer') }}</td>
+            </tr>
+        </tbody>
+        <template v-else>
+            <tbody class="subfolders">
+                <file-chooser-folder-item v-for="folder in sortedFolders" :key="folder.id" :folder="folder" tag="tr" />
+            </tbody>
+            <tbody class="files">
+                <file-chooser-file-item
+                    v-for="file in sortedFiles"
+                    :key="file.id"
+                    :file="file"
+                    tag="tr"
+                    @selectId="$emit('selectId')"
+                />
+            </tbody>
+        </template>
+    </table>
+</template>
+
+<script>
+import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js';
+import fileChooserFileItem from './FileChooserFileItem.vue';
+import fileChooserFolderItem from './FileChooserFolderItem.vue';
+
+export default {
+    name: 'file-chooser-table',
+    mixins: [folderIconMixin],
+    components: {
+        fileChooserFileItem,
+        fileChooserFolderItem,
+    },
+    props: {
+        files: {
+            type: Array,
+            required: false,
+        },
+        subfolders: {
+            type: Array,
+            required: false,
+        },
+        empty: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    data() {
+        return {
+            sortBy: 'name',
+            sortASC: true,
+        };
+    },
+    computed: {
+        sortedFiles() {
+            let files = this.files;
+            switch (this.sortBy) {
+                case 'name':
+                    files = files.sort((a, b) => {
+                        const aName = a.attributes.name.toUpperCase();
+                        const bName = b.attributes.name.toUpperCase();
+                        if (this.sortASC) {
+                            return aName.localeCompare(bName, 'de', { sensitivity: 'base' });
+                        } else {
+                            return bName.localeCompare(aName, 'de', { sensitivity: 'base' });
+                        }
+                    });
+                    break;
+                case 'size':
+                    files = files.sort((a, b) => {
+                        if (this.sortASC) {
+                            return a.attributes.filesize < b.attributes.filesize ? -1 : 1;
+                        } else {
+                            return a.attributes.filesize > b.attributes.filesize ? -1 : 1;
+                        }
+                    });
+                    break;
+                case 'owner':
+                    files = files.sort((a, b) => {
+                        const aName = (a.relationships.owner?.meta?.name ?? '').toUpperCase();
+                        const bName = (b.relationships.owner?.meta?.name ?? '').toUpperCase();
+                        if (this.sortASC) {
+                            return aName.localeCompare(bName, 'de', {
+                                sensitivity: 'base',
+                            });
+                        } else {
+                            return bName.localeCompare(aName, 'de', {
+                                sensitivity: 'base',
+                            });
+                        }
+                    });
+                    break;
+                case 'mkdate':
+                    files = files.sort((a, b) => {
+                        if (this.sortASC) {
+                            return new Date(a.attributes.mkdate) < new Date(b.attributes.mkdate) ? -1 : 1;
+                        } else {
+                            return new Date(a.attributes.mkdate) > new Date(b.attributes.mkdate) ? -1 : 1;
+                        }
+                    });
+                    break;
+            }
+            return files;
+        },
+        sortedFolders() {
+            let folders = this.subfolders;
+            switch (this.sortBy) {
+                case 'name':
+                    folders = folders.sort((a, b) => {
+                        const aName = a.attributes.name.toUpperCase();
+                        const bName = b.attributes.name.toUpperCase();
+                        if (this.sortASC) {
+                            return aName.localeCompare(bName, 'de', { sensitivity: 'base' });
+                        } else {
+                            return bName.localeCompare(aName, 'de', { sensitivity: 'base' });
+                        }
+                    });
+                    break;
+                case 'size':
+                    folders = folders.sort((a, b) => {
+                        const aObjects = a.relationships['file-refs'].meta.count + a.relationships.folders.meta.count;
+                        const bObjects = b.relationships['file-refs'].meta.count + b.relationships.folders.meta.count;
+                        if (this.sortASC) {
+                            return aObjects < bObjects ? -1 : 1;
+                        } else {
+                            return aObjects > bObjects ? -1 : 1;
+                        }
+                    });
+                    break;
+                case 'owner':
+                    folders = folders.sort((a, b) => {
+                        const aName = (a.relationships.owner?.meta?.name ?? '').toUpperCase();
+                        const bName = (b.relationships.owner?.meta?.name ?? '').toUpperCase();
+                        if (this.sortASC) {
+                            return aName.localeCompare(bName, 'de', {
+                                sensitivity: 'base',
+                            });
+                        } else {
+                            return bName.localeCompare(aName, 'de', {
+                                sensitivity: 'base',
+                            });
+                        }
+                    });
+                    break;
+                case 'mkdate':
+                    folders = folders.sort((a, b) => {
+                        if (this.sortASC) {
+                            return new Date(a.attributes.mkdate) < new Date(b.attributes.mkdate) ? -1 : 1;
+                        } else {
+                            return new Date(a.attributes.mkdate) > new Date(b.attributes.mkdate) ? -1 : 1;
+                        }
+                    });
+                    break;
+            }
+            return folders;
+        },
+    },
+    methods: {
+        sort(sortBy) {
+            if (this.sortBy === sortBy) {
+                this.sortASC = !this.sortASC;
+            } else {
+                this.sortBy = sortBy;
+            }
+        },
+        getSortClass(col) {
+            if (col === this.sortBy) {
+                return this.sortASC ? 'sortasc' : 'sortdesc';
+            }
+        },
+    },
+};
+</script>
diff --git a/resources/vue/components/file-chooser/FileChooserToolbar.vue b/resources/vue/components/file-chooser/FileChooserToolbar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4f3053e94288876d5177c8052fa333d6dbc70e2a
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserToolbar.vue
@@ -0,0 +1,224 @@
+<template>
+    <div class="file-chooser-toolbar">
+        <button v-if="showButtons" class="button" :disabled="!canAddFolder" @click="addFolder">
+            {{ $gettext('Ordner hinzufügen') }}
+        </button>
+        <form v-if="showFolderAdder" class="inline-form" @submit.prevent="">
+            <label for="file-chooser-add-folder">{{ $gettext('Ordner hinzufügen') }}:</label>
+            <input
+                id="file-chooser-add-folder"
+                type="text"
+                v-model="newFolderName"
+                :placeholder="$gettext('Ordnername')"
+            />
+            <div class="inline-buttons">
+                <button :title="$gettext('Ordner anlegen')" @click="createFolder">
+                    <studip-icon shape="accept" />
+                </button>
+                <button :title="$gettext('Abbrechen')" @click="closeAddFolder"><studip-icon shape="decline" /></button>
+            </div>
+        </form>
+        <button v-if="showButtons && !isFolderChooser" class="button" @click="$refs.fileInput.click()">
+            {{ $gettext('Datei hinzufügen') }}
+        </button>
+        <input v-show="false" type="file" ref="fileInput" :disabled="!canAddFile" @change="updateUpload" />
+        <form v-if="showUpload" class="inline-form" @submit.prevent="">
+            <label for="file-chooser-add-file">{{ $gettext('Datei hinzufügen') }}:</label>
+            <input
+                id="file-chooser-add-file"
+                :title="$gettext('Datei auswählen')"
+                type="text"
+                :value="uploadFileName"
+                readonly
+                @click="$refs.fileInput.click()"
+            />
+            <div class="inline-buttons">
+                <button :title="$gettext('Datei hochladen')" @click="createFile"><studip-icon shape="accept" /></button>
+                <button :title="$gettext('Abbrechen')" @click="closeAddFile"><studip-icon shape="decline" /></button>
+            </div>
+        </form>
+    </div>
+</template>
+
+<script>
+import axios from 'axios';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'file-chooser-toolbar',
+    data() {
+        return {
+            showFolderAdder: false,
+            newFolderName: '',
+            showUpload: false,
+            uploadFile: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            activeFolderId: 'file-chooser/activeFolderId',
+            activeFolder: 'file-chooser/activeFolder',
+            activeFolderRangeType: 'file-chooser/activeFolderRangeType',
+            courseId: 'file-chooser/courseId',
+            isFolderChooser: 'file-chooser/isFolderChooser',
+            userId: 'file-chooser/userId',
+        }),
+        showButtons() {
+            return !this.showUpload && !this.showFolderAdder;
+        },
+        canAddFolder() {
+            if (this.activeFolder) {
+                return (
+                    this.activeFolder.attributes['is-writable'] && this.activeFolder.attributes['is-subfolder-allowed']
+                );
+            }
+            return false;
+        },
+        canAddFile() {
+            if (this.activeFolder) {
+                return this.activeFolder.attributes['is-writable'];
+            }
+            return false;
+        },
+        uploadFileName() {
+            return this.uploadFile.name;
+        },
+    },
+    methods: {
+        ...mapActions({
+            loadRangeFolders: 'file-chooser/loadRangeFolders',
+            loadFolderFiles: 'file-chooser/loadFolderFiles',
+        }),
+        addFolder() {
+            this.showFolderAdder = true;
+        },
+        closeAddFolder() {
+            this.showFolderAdder = false;
+            this.newFolderName = '';
+        },
+        async createFolder() {
+            if (this.newFolderName === '') {
+                this.closeAddFolder();
+            }
+            this.showFolderAdder = false;
+            const httpClient = await this.getHttpClient();
+            const newFolder = {
+                data: {
+                    type: 'folders',
+                    attributes: {
+                        name: this.newFolderName,
+                        'folder-type': 'StandardFolder',
+                    },
+                    relationships: {
+                        parent: {
+                            data: {
+                                id: this.activeFolderId,
+                                type: 'folders',
+                            },
+                        },
+                    },
+                },
+            };
+            const context = {
+                type: this.activeFolderRangeType,
+                id: this.activeFolderRangeType === 'users' ? this.userId : this.courseId,
+            };
+            await httpClient.post(`${context.type}/${context.id}/folders`, newFolder);
+            this.$emit('folderAdded');
+            this.newFolderName = '';
+            this.loadRangeFolders({ rangeType: context.type, rangeId: context.id });
+        },
+        getHttpClient() {
+            return axios.create({
+                baseURL: STUDIP.URLHelper.getURL(`jsonapi.php/v1`, {}, true),
+                headers: {
+                    'Content-Type': 'application/vnd.api+json',
+                },
+            });
+        },
+        updateUpload() {
+            this.showUpload = true;
+            this.uploadFile = this.$refs.fileInput.files[0];
+        },
+        closeAddFile() {
+            this.showUpload = false;
+            this.$refs.fileInput.value = null;
+        },
+        async createFile() {
+            this.showUpload = false;
+            const httpClient = await this.getHttpClient();
+            const formData = new FormData();
+            formData.append('file', this.uploadFile, this.uploadFileName);
+            const url = `folders/${this.activeFolderId}/file-refs`;
+            let request = await httpClient.post(url, formData, {
+                headers: {
+                    'Content-Type': 'multipart/form-data',
+                },
+            });
+            let response = null;
+            try {
+                response = await httpClient.get(request.headers.location);
+            } catch (e) {
+                console.debug(e);
+                response = null;
+            }
+
+            await this.loadFolderFiles({ folderId: this.activeFolderId });
+            this.$emit('fileAdded');
+            this.$refs.fileInput.value = null;
+        },
+    },
+    watch: {
+        activeFolderId(newId) {
+            this.closeAddFolder();
+            this.closeAddFile();
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.file-chooser-toolbar {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    border-top: solid thin var(--content-color-40);
+
+    &.with-table {
+        border: none;
+        margin-top: -16px;
+    }
+
+    .inline-form {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: wrap;
+        justify-content: start;
+        gap: 4px;
+        width: 100%;
+        margin: 0.8em 0.6em 0.8em 0;
+
+        label {
+            line-height: 30px;
+        }
+
+        input {
+            flex-grow: 1;
+            padding: 4px;
+            border: solid thin var(--content-color-40);
+            border-radius: 0;
+        }
+        button {
+            border: solid thin var(--base-color);
+            background-color: transparent;
+            height: 30px;
+            width: 30px;
+            cursor: pointer;
+
+            img {
+                vertical-align: middle;
+            }
+        }
+    }
+}
+</style>
diff --git a/resources/vue/components/file-chooser/FileChooserTree.vue b/resources/vue/components/file-chooser/FileChooserTree.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b3e2d525be3b736a3a0bc023e67e69e4f4dbca44
--- /dev/null
+++ b/resources/vue/components/file-chooser/FileChooserTree.vue
@@ -0,0 +1,98 @@
+<template v-if="folderIsReadable">
+    <li class="file-chooser-tree-item">
+        <span class="folder-toggle">
+            <a
+                v-if="hasSubfolders"
+                herf="#"
+                @click.prevent="toggleSubfolders"
+                :title="unfold ? $gettext('Ordner zuklappen') : $gettext('Ordner aufklappen')"
+            >
+                <studip-icon :shape="unfold ? 'arr_1down' : 'arr_1right'" />
+            </a>
+        </span>
+        <a href="#" @click.prevent="selectFolder" :class="{ selected: isSelected }">
+            <studip-icon :shape="folderIcon" />
+            <span>{{ folder.attributes.name }}</span>
+        </a>
+        <ul v-if="unfold" class="file-chooser-tree">
+            <li v-for="child in folder.children" :key="child.id" class="file-chooser-tree-item">
+                <file-chooser-tree :folder="child" />
+            </li>
+        </ul>
+    </li>
+</template>
+
+<script>
+import folderIconMixin from '@/vue/mixins/file-chooser/folder-icon.js';
+import { mapActions, mapGetters } from 'vuex';
+export default {
+    name: 'file-chooser-tree',
+    mixins: [folderIconMixin],
+    props: {
+        folder: Object,
+    },
+    data() {
+        return {
+            unfold: false,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            activeFolderId: 'file-chooser/activeFolderId',
+        }),
+        isSelected() {
+            return this.folder.id === this.activeFolderId;
+        },
+        hasSubfolders() {
+            const counter = this.folder.relationships?.folders?.meta?.count ?? 0;
+
+            return counter > 0;
+        },
+    },
+    methods: {
+        ...mapActions({
+            setActiveFolderId: 'file-chooser/setActiveFolderId',
+            setSelectedFolderId: 'file-chooser/setSelectedFolderId',
+        }),
+        selectFolder() {
+            this.setActiveFolderId(this.folder.id);
+            this.setSelectedFolderId('');
+            this.unfold = true;
+        },
+        toggleSubfolders() {
+            this.unfold = !this.unfold;
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.file-chooser-tree {
+    padding-left: 18px;
+    &.file-chooser-tree-first-level {
+        padding-left: 0;
+    }
+}
+.file-chooser-tree-item {
+    list-style: none;
+    padding: 2px 0 0 0;
+    .folder-toggle {
+        width: 16px;
+    }
+    a.selected {
+        font-weight: 700;
+    }
+    img {
+        vertical-align: middle;
+    }
+    span {
+        width: calc(100% - 46px);
+        display: inline-block;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        height: 16px;
+        white-space: nowrap;
+        vertical-align: sub;
+    }
+}
+</style>
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 3309b55ce47f16e98983da45e904cab1d112523e..e32baed7d259150b1d0876c32e3d3e1bcd294016 100644
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -1,5 +1,6 @@
 import CoursewareModule from './store/courseware/courseware.module';
 import CoursewareStructureModule from './store/courseware/structure.module';
+import FileChooserStore from './store/file-chooser.js';
 import CoursewareStructuralElement from './components/courseware/structural-element/CoursewareStructuralElement.vue';
 import IndexApp from './components/courseware/IndexApp.vue';
 import PluginManager from './components/courseware/plugin-manager.js';
@@ -82,6 +83,7 @@ const mountApp = async (STUDIP, createApp, element) => {
         modules: {
             courseware: CoursewareModule,
             'courseware-structure': CoursewareStructureModule,
+            'file-chooser': FileChooserStore,
             ...mapResourceModules({
                 names: [
                     'courses',
diff --git a/resources/vue/mixins/courseware/block.js b/resources/vue/mixins/courseware/block.js
index 0a83906e742831004a01894866fe4a89c08de885..c49a69789b19175a8dec774b4685a746b5a511d7 100644
--- a/resources/vue/mixins/courseware/block.js
+++ b/resources/vue/mixins/courseware/block.js
@@ -4,6 +4,8 @@ const blockMixin = {
     computed: {
         ...mapGetters({
             getUserProgress: 'courseware-user-progresses/related',
+            context: 'context',
+            userId: 'userId',
         }),
         userProgress: {
             get: function () {
@@ -15,6 +17,9 @@ const blockMixin = {
                 return this.updateUserProgress(this.userProgress);
             },
         },
+        excludedCourseFolderTypes() {
+            return ['HomeworkFolder'];
+        }
     },
     methods: {
         ...mapActions({
diff --git a/resources/vue/mixins/file-chooser/folder-icon.js b/resources/vue/mixins/file-chooser/folder-icon.js
new file mode 100644
index 0000000000000000000000000000000000000000..be23dfa3a52a4d2047e922772732d028d7d58134
--- /dev/null
+++ b/resources/vue/mixins/file-chooser/folder-icon.js
@@ -0,0 +1,56 @@
+const folderIconMixin = {
+    computed: {
+        folderName() {
+            return this.folder.attributes.name;
+        },
+        folderType() {
+            return this.folder.attributes['folder-type'];
+        },
+        folderIsEmpty() {
+            return this.folder.attributes['is-empty'];
+        },
+        folderIsReadable() {
+            return this.folder.attributes['is-readable'];
+        },
+        folderIcon() {
+            let shape = 'folder';
+
+            switch (this.folderType) {
+                case 'HomeworkFolder':
+                case 'HiddenFolder':
+                    shape = 'folder-lock';
+                    break;
+                case 'CourseGroupFolder':
+                    shape = 'folder-group';
+                    break;
+                case 'TimedFolder':
+                    shape = 'folder-date';
+                    break;
+                case 'CourseDateFolder':
+                    shape = 'folder-topic';
+                    break;
+                case 'MaterialFolder':
+                    return 'download';
+                case 'PublicFolder':
+                case 'CoursePublicFolder':
+                    shape = 'folder-public';
+                    break;
+                case 'InboxFolder':
+                case 'InboxOutboxFolder':
+                    shape = 'folder-inbox';
+                    break;
+            }
+
+            if (this.folderIsEmpty) {
+                shape += '-empty';
+            } else {
+                shape += '-full';
+            }
+
+            return shape;
+        }
+    },
+
+};
+
+export default folderIconMixin;
\ No newline at end of file
diff --git a/resources/vue/store/file-chooser.js b/resources/vue/store/file-chooser.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8554b3b01bb9346fdcf999bb80c2a066efb1997
--- /dev/null
+++ b/resources/vue/store/file-chooser.js
@@ -0,0 +1,248 @@
+const getDefaultState = () => {
+    return {
+        selectable: 'file',
+        selectedFileId: '',
+        selectedFolderId: '',
+        activeFolderId: '',
+        userId: '',
+        courseId: '',
+        isAudio: false,
+        isDocument: false,
+        isImage: false,
+        isVideo: false,
+    };
+};
+
+const initialState = getDefaultState();
+const state = { ...initialState };
+
+const getters = {
+    selectable(state) {
+        return state.selectable;
+    },
+    selectedFileId(state) {
+        return state.selectedFileId;
+    },
+    selectedFolderId(state) {
+        return state.selectedFolderId;
+    },
+    activeFolderId(state) {
+        return state.activeFolderId;
+    },
+    userId(state) {
+        return state.userId;
+    },
+    courseId(state) {
+        return state.courseId;
+    },
+    isAudio(state) {
+        return state.isAudio;
+    },
+    isDocument(state) {
+        return state.isDocument;
+    },
+    isImage(state) {
+        return state.isImage;
+    },
+    isVideo(state) {
+        return state.isVideo;
+    },
+
+    activeFolder(state, getters, rootState, rootGetters) {
+        const id = state.activeFolderId;
+        if (id) {
+            return rootGetters['folders/byId']({ id });
+        }
+
+        return null;
+    },
+
+    activeFolderRangeType(state, getters) {
+        return getters.activeFolder?.relationships?.range?.data?.type;
+    },
+
+    relatedUsersFolders(state, getters, rootState, rootGetters) {
+        const parent = { type: 'users', id: getters.userId };
+        const relationship = 'folders';
+        return rootGetters['folders/related']({ parent, relationship });
+    },
+
+    relatedCoursesFolders(state, getters, rootState, rootGetters) {
+        const parent = { type: 'courses', id: getters.courseId };
+        const relationship = 'folders';
+        return rootGetters['folders/related']({ parent, relationship });
+    },
+    filterActive(state, getters) {
+        return getters.isAudio || getters.isDocument || getters.isImage || getters.isVideo;
+    },
+    currentFolderFiles(state, getters, rootState, rootGetters) {
+        const id = state.activeFolderId;
+        if (id === '') {
+            return [];
+        }
+        const parent = { type: 'folders', id: id };
+        const relationship = 'file-refs';
+        let files = rootGetters['file-refs/related']({ parent, relationship }) ?? [];
+
+        if (!getters.filterActive) {
+            return files;
+        }
+
+        files =
+            files.filter((file) => {
+                const fileTermsOfUse = rootGetters['terms-of-use/related']({
+                    parent: file,
+                    relationship: 'terms-of-use',
+                });
+                if (fileTermsOfUse !== null && fileTermsOfUse.attributes['download-condition'] !== 0) {
+                    return false;
+                }
+                if (getters.isImage && !file.attributes['mime-type'].includes('image')) {
+                    return false;
+                }
+                const videoConditions = ['video/mp4', 'video/ogg', 'video/webm'];
+                if (
+                    getters.isVideo &&
+                    !videoConditions.some((condition) => file.attributes['mime-type'].includes(condition))
+                ) {
+                    return false;
+                }
+                const audioConditions = [
+                    'audio/wav',
+                    'audio/ogg',
+                    'audio/webm',
+                    'audio/flac',
+                    'audio/mpeg',
+                    'audio/x-m4a',
+                    'audio/mp4',
+                ];
+                if (
+                    getters.isAudio
+                    && !audioConditions.some((condition) => file.attributes['mime-type'].includes(condition))
+                ) {
+                    return false;
+                }
+                const officeConditions = ['application/pdf']; //TODO enable more mime types
+                if (
+                    getters.isDocument
+                    && !officeConditions.some((condition) => file.attributes['mime-type'].includes(condition))
+                ) {
+                    return false;
+                }
+
+                return true;
+            }) ?? [];
+
+        return files;
+    },
+    isFolderChooser(state, getters) {
+        return getters.selectable === 'folder';
+    },
+};
+
+const actions = {
+    //setters
+    setSelectable({ commit }, value) {
+        commit('setSelectable', value);
+    },
+    setSelectedFileId({ commit }, id) {
+        commit('setSelectedFileId', id);
+    },
+    setSelectedFolderId({ commit }, id) {
+        commit('setSelectedFolderId', id);
+    },
+    setActiveFolderId({ commit }, id) {
+        commit('setActiveFolderId', id);
+    },
+    setCourseId({ commit }, id) {
+        commit('setCourseId', id);
+    },
+    setUserId({ commit }, id) {
+        commit('setUserId', id);
+    },
+    setIsAudio({ commit }, id) {
+        commit('setIsAudio', id);
+    },
+    setIsDocument({ commit }, id) {
+        commit('setIsDocument', id);
+    },
+    setIsImage({ commit }, id) {
+        commit('setIsImage', id);
+    },
+    setIsVideo({ commit }, id) {
+        commit('setIsVideo', id);
+    },
+    // custom action
+    async loadRangeFolders({ dispatch }, { rangeType, rangeId }) {
+        const parent = { type: rangeType, id: rangeId };
+        const relationship = 'folders';
+        const options = { 'page[limit]': 10000 };
+
+        return dispatch(
+            'folders/loadRelated',
+            {
+                parent,
+                relationship,
+                options,
+            },
+            { root: true }
+        );
+    },
+
+    loadFolderFiles({ dispatch }, { folderId }) {
+        const parent = { type: 'folders', id: folderId };
+        const relationship = 'file-refs';
+        const options = { include: 'terms-of-use', 'page[limit]': 10000 };
+        return dispatch(
+            'file-refs/loadRelated',
+            {
+                parent,
+                relationship,
+                options,
+            },
+            { root: true }
+        );
+    },
+};
+
+export const mutations = {
+    setSelectable(state, data) {
+        state.selectable = data;
+    },
+    setSelectedFileId(state, data) {
+        state.selectedFileId = data;
+    },
+    setSelectedFolderId(state, data) {
+        state.selectedFolderId = data;
+    },
+    setActiveFolderId(state, data) {
+        state.activeFolderId = data;
+        state.selectedFileId = '';
+    },
+    setCourseId(state, data) {
+        state.courseId = data;
+    },
+    setUserId(state, data) {
+        state.userId = data;
+    },
+    setIsAudio(state, data) {
+        state.isAudio = data;
+    },
+    setIsDocument(state, data) {
+        state.isDocument = data;
+    },
+    setIsImage(state, data) {
+        state.isImage = data;
+    },
+    setIsVideo(state, data) {
+        state.isVideo = data;
+    },
+};
+
+export default {
+    namespaced: true,
+    actions,
+    getters,
+    mutations,
+    state,
+};