diff --git a/src/addons/mod/folder/components/components.module.ts b/src/addons/mod/folder/components/components.module.ts
new file mode 100644
index 000000000..1137d911a
--- /dev/null
+++ b/src/addons/mod/folder/components/components.module.ts
@@ -0,0 +1,34 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+
+import { CoreSharedModule } from '@/core/shared.module';
+import { CoreCourseComponentsModule } from '@features/course/components/components.module';
+
+import { AddonModFolderIndexComponent } from './index/index';
+
+@NgModule({
+ declarations: [
+ AddonModFolderIndexComponent,
+ ],
+ imports: [
+ CoreSharedModule,
+ CoreCourseComponentsModule,
+ ],
+ exports: [
+ AddonModFolderIndexComponent,
+ ],
+})
+export class AddonModFolderComponentsModule {}
diff --git a/src/addons/mod/folder/components/index/addon-mod-folder-index.html b/src/addons/mod/folder/components/index/addon-mod-folder-index.html
new file mode 100644
index 000000000..992d5370f
--- /dev/null
+++ b/src/addons/mod/folder/components/index/addon-mod-folder-index.html
@@ -0,0 +1,49 @@
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0)">
+
+
+
+
+ {{folder.filename}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addons/mod/folder/components/index/index.ts b/src/addons/mod/folder/components/index/index.ts
new file mode 100644
index 000000000..6bb72670a
--- /dev/null
+++ b/src/addons/mod/folder/components/index/index.ts
@@ -0,0 +1,137 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { CoreConstants } from '@/core/constants';
+import { Component, Input, OnInit, Optional } from '@angular/core';
+import { Params } from '@angular/router';
+import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component';
+import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreApp } from '@services/app';
+import { CoreNavigator } from '@services/navigator';
+import { Md5 } from 'ts-md5';
+import { AddonModFolder, AddonModFolderFolder, AddonModFolderProvider } from '../../services/folder';
+import { AddonModFolderFolderFormattedData, AddonModFolderHelper } from '../../services/folder-helper';
+
+/**
+ * Component that displays a folder.
+ *
+ * @todo Adding a new file in a folder updates the revision of all the files, so they're all shown as outdated.
+ * To ignore revision in folders we'll have to modify CoreCourseModulePrefetchDelegate, core-file and CoreFilepoolProvider.
+ */
+@Component({
+ selector: 'addon-mod-folder-index',
+ templateUrl: 'addon-mod-folder-index.html',
+})
+export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
+
+ @Input() folderInstance?: AddonModFolderFolder; // The mod_folder instance.
+ @Input() subfolder?: AddonModFolderFolderFormattedData; // Subfolder to show.
+
+ component = AddonModFolderProvider.COMPONENT;
+ canGetFolder = false;
+
+ constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
+ super('AddonModFolderIndexComponent', courseContentsPage);
+ }
+
+ /**
+ * Component being initialized.
+ */
+ async ngOnInit(): Promise {
+ super.ngOnInit();
+
+ this.canGetFolder = AddonModFolder.isGetFolderWSAvailable();
+
+ if (this.subfolder) {
+ this.description = this.folderInstance ? this.folderInstance.intro : this.module!.description;
+
+ this.loaded = true;
+ this.refreshIcon = CoreConstants.ICON_REFRESH;
+
+ return;
+ }
+
+ try {
+ await this.loadContent();
+
+ try {
+ await AddonModFolder.logView(this.module!.instance!, this.module!.name);
+ CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata);
+ } catch {
+ // Ignore errors.
+ }
+ } finally {
+ this.loaded = true;
+ this.refreshIcon = CoreConstants.ICON_REFRESH;
+ }
+ }
+
+ /**
+ * Perform the invalidate content function.
+ *
+ * @return Resolved when done.
+ */
+ protected async invalidateContent(): Promise {
+ await AddonModFolder.invalidateContent(this.module!.id, this.courseId!);
+ }
+
+ /**
+ * Download folder contents.
+ *
+ * @param refresh Whether we're refreshing data.
+ * @return Promise resolved when done.
+ */
+ protected async fetchContent(refresh = false): Promise {
+ try {
+ if (this.canGetFolder) {
+ this.folderInstance = await AddonModFolder.getFolder(this.courseId!, this.module!.id);
+ await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh);
+ } else {
+ const module = await CoreCourse.getModule(this.module!.id, this.courseId);
+
+ if (!module.contents.length && this.module!.contents.length && !CoreApp.isOnline()) {
+ // The contents might be empty due to a cached data. Use the old ones.
+ module.contents = this.module!.contents;
+ }
+ this.module = module;
+ }
+
+ this.dataRetrieved.emit(this.folderInstance || this.module);
+
+ this.description = this.folderInstance ? this.folderInstance.intro : this.module!.description;
+ this.subfolder = AddonModFolderHelper.formatContents(this.module!.contents);
+ } finally {
+ this.fillContextMenu(refresh);
+ }
+ }
+
+ /**
+ * Navigate to a subfolder.
+ *
+ * @param folder Folder data.
+ */
+ openFolder(folder: AddonModFolderFolderFormattedData): void {
+ const params: Params = {
+ module: this.module,
+ folderInstance: this.folderInstance,
+ subfolder: folder,
+ };
+
+ const hash = Md5.hashAsciiStr(folder.filepath);
+
+ CoreNavigator.navigate('../' + hash, { params });
+ }
+
+}
diff --git a/src/addons/mod/folder/folder-lazy.module.ts b/src/addons/mod/folder/folder-lazy.module.ts
new file mode 100644
index 000000000..55748b89d
--- /dev/null
+++ b/src/addons/mod/folder/folder-lazy.module.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { CoreSharedModule } from '@/core/shared.module';
+import { AddonModFolderComponentsModule } from './components/components.module';
+
+import { AddonModFolderIndexPage } from './pages/index/index.page';
+
+const routes: Routes = [
+ {
+ path: ':courseId/:cmId/:hash',
+ component: AddonModFolderIndexPage,
+ },
+ {
+ path: ':courseId/:cmId',
+ redirectTo: ':courseId/:cmId/', // Fake "hash".
+ pathMatch: 'full',
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CoreSharedModule,
+ AddonModFolderComponentsModule,
+ ],
+ declarations: [
+ AddonModFolderIndexPage,
+ ],
+})
+export class AddonModFolderLazyModule {}
diff --git a/src/addons/mod/folder/folder.module.ts b/src/addons/mod/folder/folder.module.ts
new file mode 100644
index 000000000..fb5395403
--- /dev/null
+++ b/src/addons/mod/folder/folder.module.ts
@@ -0,0 +1,56 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { Routes } from '@angular/router';
+import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
+import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
+import { CorePluginFileDelegate } from '@services/plugin-file-delegate';
+import { AddonModFolderComponentsModule } from './components/components.module';
+import { AddonModFolderIndexLinkHandler } from './services/handlers/index-link';
+import { AddonModFolderListLinkHandler } from './services/handlers/list-link';
+import { AddonModFolderModuleHandler, AddonModFolderModuleHandlerService } from './services/handlers/module';
+import { AddonModFolderPluginFileHandler } from './services/handlers/pluginfile';
+import { AddonModFolderPrefetchHandler } from './services/handlers/prefetch';
+
+const routes: Routes = [
+ {
+ path: AddonModFolderModuleHandlerService.PAGE_NAME,
+ loadChildren: () => import('./folder-lazy.module').then(m => m.AddonModFolderLazyModule),
+ },
+];
+
+@NgModule({
+ imports: [
+ CoreMainMenuTabRoutingModule.forChild(routes),
+ AddonModFolderComponentsModule,
+ ],
+ providers: [
+ {
+ provide: APP_INITIALIZER,
+ multi: true,
+ deps: [],
+ useFactory: () => () => {
+ CoreCourseModuleDelegate.registerHandler(AddonModFolderModuleHandler.instance);
+ CoreContentLinksDelegate.registerHandler(AddonModFolderIndexLinkHandler.instance);
+ CoreContentLinksDelegate.registerHandler(AddonModFolderListLinkHandler.instance);
+ CoreCourseModulePrefetchDelegate.registerHandler(AddonModFolderPrefetchHandler.instance);
+ CorePluginFileDelegate.registerHandler(AddonModFolderPluginFileHandler.instance);
+ },
+ },
+ ],
+})
+export class AddonModFolderModule {}
diff --git a/src/addons/mod/folder/lang.json b/src/addons/mod/folder/lang.json
new file mode 100644
index 000000000..40bcdb36a
--- /dev/null
+++ b/src/addons/mod/folder/lang.json
@@ -0,0 +1,4 @@
+{
+ "emptyfilelist": "There are no files to show.",
+ "modulenameplural": "Folders"
+}
\ No newline at end of file
diff --git a/src/addons/mod/folder/pages/index/index.html b/src/addons/mod/folder/pages/index/index.html
new file mode 100644
index 000000000..2bdcf774b
--- /dev/null
+++ b/src/addons/mod/folder/pages/index/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addons/mod/folder/pages/index/index.page.ts b/src/addons/mod/folder/pages/index/index.page.ts
new file mode 100644
index 000000000..7b7f3fce7
--- /dev/null
+++ b/src/addons/mod/folder/pages/index/index.page.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
+import { CoreNavigator } from '@services/navigator';
+import { AddonModFolderIndexComponent } from '../../components/index';
+import { AddonModFolderFolder } from '../../services/folder';
+import { AddonModFolderFolderFormattedData } from '../../services/folder-helper';
+
+/**
+ * Page that displays a folder.
+ */
+@Component({
+ selector: 'page-addon-mod-folder-index',
+ templateUrl: 'index.html',
+})
+export class AddonModFolderIndexPage extends CoreCourseModuleMainActivityPage implements OnInit {
+
+ @ViewChild(AddonModFolderIndexComponent) activityComponent?: AddonModFolderIndexComponent;
+
+ folderInstance?: AddonModFolderFolder;
+ subfolder?: AddonModFolderFolderFormattedData;
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ super.ngOnInit();
+ this.folderInstance = CoreNavigator.getRouteParam('folderInstance');
+ this.subfolder = CoreNavigator.getRouteParam('subfolder');
+ }
+
+}
diff --git a/src/addons/mod/folder/services/folder-helper.ts b/src/addons/mod/folder/services/folder-helper.ts
new file mode 100644
index 000000000..677d48fb5
--- /dev/null
+++ b/src/addons/mod/folder/services/folder-helper.ts
@@ -0,0 +1,99 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseModuleContentFile } from '@features/course/services/course';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Service that provides some features for folder.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderHelperProvider {
+
+ /**
+ * Format folder contents, creating directory structure.
+ * Folders found in filepaths are added to the array. Each folder has the properties: name, fileicon,
+ * type (folder), filepath and contents (array with files and subfolders).
+ *
+ * @param fileEntries Folder contents.
+ * @return Formatted contents.
+ */
+ formatContents(fileEntries: CoreCourseModuleContentFile[]): AddonModFolderFolderFormattedData {
+ const rootFolder: AddonModFolderFolderFormattedData = {
+ type: 'root',
+ filename: '',
+ filepath: '',
+ folders: [],
+ files: [],
+ };
+
+ fileEntries.forEach((fileEntry) => {
+ // Root level. Just add.
+ if (fileEntry.filepath === '/') {
+ rootFolder.files.push(fileEntry);
+
+ return;
+ }
+
+ // It's a file in a subfolder. Lets treat the path to add the subfolders to the array.
+ let currentFolder = rootFolder; // Start at root level.
+ let path = fileEntry.filepath;
+ let completePath = '';
+
+ // Remove first and last slash if needed.
+ if (path.substr(0, 1) === '/') {
+ path = path.substr(1);
+ }
+ if (path.substr(path.length - 1) === '/') {
+ path = path.slice(0, -1);
+ }
+
+ const directories: string[] = path.split('/');
+
+ directories.forEach((directory) => {
+ completePath = completePath + '/' + directory;
+ // Search if the directory is already stored in folders array.
+ let subFolder = currentFolder.folders.find((list) => list.filename === directory);
+
+ if (!subFolder) {
+ // Directory not found. Add it to the array.
+ subFolder = {
+ type: 'folder',
+ filename: directory,
+ filepath: completePath,
+ folders: [],
+ files: [],
+ };
+ currentFolder.folders.push(subFolder);
+ }
+ currentFolder = subFolder;
+ });
+
+ currentFolder.files.push(fileEntry);
+ });
+
+ return rootFolder;
+ }
+
+}
+export const AddonModFolderHelper = makeSingleton(AddonModFolderHelperProvider);
+
+export type AddonModFolderFolderFormattedData = {
+ type: string; // A file or a folder or external link.
+ filename: string;
+ filepath: string;
+ folders: AddonModFolderFolderFormattedData[];
+ files: CoreCourseModuleContentFile[];
+};
diff --git a/src/addons/mod/folder/services/folder.ts b/src/addons/mod/folder/services/folder.ts
new file mode 100644
index 000000000..ea3ca2c07
--- /dev/null
+++ b/src/addons/mod/folder/services/folder.ts
@@ -0,0 +1,206 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreError } from '@classes/errors/error';
+import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreCourseLogHelper } from '@features/course/services/log-helper';
+import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
+import { makeSingleton } from '@singletons';
+
+const ROOT_CACHE_KEY = 'mmaModFolder:';
+
+/**
+ * Service that provides some features for folder.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderProvider {
+
+ static readonly COMPONENT = 'mmaModFolder';
+
+ /**
+ * Get a folder by course module ID.
+ *
+ * @param courseId Course ID.
+ * @param cmId Course module ID.
+ * @param options Other options.
+ * @return Promise resolved when the book is retrieved.
+ */
+ getFolder(courseId: number, cmId: number, options?: CoreSitesCommonWSOptions): Promise {
+ return this.getFolderByKey(courseId, 'coursemodule', cmId, options);
+ }
+
+ /**
+ * Get a folder.
+ *
+ * @param courseId Course ID.
+ * @param key Name of the property to check.
+ * @param value Value to search.
+ * @param options Other options.
+ * @return Promise resolved when the book is retrieved.
+ */
+ protected async getFolderByKey(
+ courseId: number,
+ key: string,
+ value: number,
+ options: CoreSitesCommonWSOptions = {},
+ ): Promise {
+ const site = await CoreSites.getSite(options.siteId);
+
+ const params: AddonModFolderGetFoldersByCoursesWSParams = {
+ courseids: [courseId],
+ };
+
+ const preSets: CoreSiteWSPreSets = {
+ cacheKey: this.getFolderCacheKey(courseId),
+ updateFrequency: CoreSite.FREQUENCY_RARELY,
+ component: AddonModFolderProvider.COMPONENT,
+ ...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
+ };
+
+ const response =
+ await site.read('mod_folder_get_folders_by_courses', params, preSets);
+
+ const currentFolder = response.folders.find((folder) => folder[key] == value);
+ if (currentFolder) {
+ return currentFolder;
+ }
+
+ throw new CoreError('Folder not found');
+ }
+
+ /**
+ * Get cache key for folder data WS calls.
+ *
+ * @param courseId Course ID.
+ * @return Cache key.
+ */
+ protected getFolderCacheKey(courseId: number): string {
+ return ROOT_CACHE_KEY + 'folder:' + courseId;
+ }
+
+ /**
+ * Invalidate the prefetched content.
+ *
+ * @param moduleId The module ID.
+ * @param courseId Course ID of the module.
+ * @param siteId Site ID. If not defined, current site.
+ */
+ async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise {
+ const promises: Promise[] = [];
+
+ promises.push(this.invalidateFolderData(courseId, siteId));
+ promises.push(CoreCourse.invalidateModule(moduleId, siteId));
+
+ await CoreUtils.allPromises(promises);
+ }
+
+ /**
+ * Invalidates folder data.
+ *
+ * @param courseId Course ID.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when the data is invalidated.
+ */
+ async invalidateFolderData(courseId: number, siteId?: string): Promise {
+ const site = await CoreSites.getSite(siteId);
+
+ await site.invalidateWsCacheForKey(this.getFolderCacheKey(courseId));
+ }
+
+ /**
+ * Returns whether or not getFolder WS available or not.
+ *
+ * @return If WS is avalaible.
+ * @since 3.3
+ */
+ isGetFolderWSAvailable(): boolean {
+ return CoreSites.wsAvailableInCurrentSite('mod_folder_get_folders_by_courses');
+ }
+
+ /**
+ * Report a folder as being viewed.
+ *
+ * @param id Module ID.
+ * @param name Name of the folder.
+ * @param siteId Site ID. If not defined, current site.
+ * @return Promise resolved when the WS call is successful.
+ */
+ async logView(id: number, name?: string, siteId?: string): Promise {
+ const params: AddonModFolderViewFolderWSParams = {
+ folderid: id,
+ };
+
+ await CoreCourseLogHelper.logSingle(
+ 'mod_folder_view_folder',
+ params,
+ AddonModFolderProvider.COMPONENT,
+ id,
+ name,
+ 'folder',
+ {},
+ siteId,
+ );
+ }
+
+}
+export const AddonModFolder = makeSingleton(AddonModFolderProvider);
+
+
+/**
+ * Folder returned by mod_folder_get_folders_by_courses.
+ */
+export type AddonModFolderFolder = {
+ id: number; // Module id.
+ coursemodule: number; // Course module id.
+ course: number; // Course id.
+ name: string; // Page name.
+ intro: string; // Summary.
+ introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
+ introfiles: CoreWSExternalFile[];
+ revision: number; // Incremented when after each file changes, to avoid cache.
+ timemodified: number; // Last time the folder was modified.
+ display: number; // Display type of folder contents on a separate page or inline.
+ showexpanded: number; // 1 = expanded, 0 = collapsed for sub-folders.
+ showdownloadfolder: number; // Whether to show the download folder button.
+ section: number; // Course section id.
+ visible: number; // Module visibility.
+ groupmode: number; // Group mode.
+ groupingid: number; // Grouping id.
+};
+
+/**
+ * Params of mod_folder_get_folders_by_courses WS.
+ */
+type AddonModFolderGetFoldersByCoursesWSParams = {
+ courseids?: number[]; // Array of course ids.
+};
+
+/**
+ * Data returned by mod_folder_get_folders_by_courses WS.
+ */
+type AddonModFolderGetFoldersByCoursesWSResponse = {
+ folders: AddonModFolderFolder[];
+ warnings?: CoreWSExternalWarning[];
+};
+
+/**
+ * Params of mod_folder_view_folder WS.
+ */
+type AddonModFolderViewFolderWSParams = {
+ folderid: number; // Folder instance id.
+};
diff --git a/src/addons/mod/folder/services/handlers/index-link.ts b/src/addons/mod/folder/services/handlers/index-link.ts
new file mode 100644
index 000000000..ca64388db
--- /dev/null
+++ b/src/addons/mod/folder/services/handlers/index-link.ts
@@ -0,0 +1,32 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Handler to treat links to resource.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
+
+ name = 'AddonModFolderLinkHandler';
+
+ constructor() {
+ super('AddonModFolder', 'folder', 'f');
+ }
+
+}
+export const AddonModFolderIndexLinkHandler = makeSingleton(AddonModFolderIndexLinkHandlerService);
diff --git a/src/addons/mod/folder/services/handlers/list-link.ts b/src/addons/mod/folder/services/handlers/list-link.ts
new file mode 100644
index 000000000..688f6c03a
--- /dev/null
+++ b/src/addons/mod/folder/services/handlers/list-link.ts
@@ -0,0 +1,32 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Handler to treat links to folder list page.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderListLinkHandlerService extends CoreContentLinksModuleListHandler {
+
+ name = 'AddonModFolderListLinkHandler';
+
+ constructor() {
+ super('AddonModFolder', 'folder');
+ }
+
+}
+export const AddonModFolderListLinkHandler = makeSingleton(AddonModFolderListLinkHandlerService);
diff --git a/src/addons/mod/folder/services/handlers/module.ts b/src/addons/mod/folder/services/handlers/module.ts
new file mode 100644
index 000000000..f00241460
--- /dev/null
+++ b/src/addons/mod/folder/services/handlers/module.ts
@@ -0,0 +1,82 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { CoreConstants } from '@/core/constants';
+import { Injectable, Type } from '@angular/core';
+import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
+import { CoreCourseModule } from '@features/course/services/course-helper';
+import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
+import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
+import { makeSingleton } from '@singletons';
+import { AddonModFolderIndexComponent } from '../../components/index';
+
+/**
+ * Handler to support folder modules.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderModuleHandlerService implements CoreCourseModuleHandler {
+
+ static readonly PAGE_NAME = 'mod_folder';
+
+ name = 'AddonModFolder';
+ modName = 'folder';
+
+ supportedFeatures = {
+ [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
+ [CoreConstants.FEATURE_GROUPS]: false,
+ [CoreConstants.FEATURE_GROUPINGS]: false,
+ [CoreConstants.FEATURE_MOD_INTRO]: true,
+ [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
+ [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
+ [CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
+ [CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
+ [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
+ };
+
+ /**
+ * @inheritdoc
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
+ return {
+ icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
+ title: module.name,
+ class: 'addon-mod_folder-handler',
+ showDownloadButton: true,
+ action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
+ options = options || {};
+ options.params = options.params || {};
+ Object.assign(options.params, { module });
+ const routeParams = '/' + courseId + '/' + module.id;
+
+ CoreNavigator.navigateToSitePath(AddonModFolderModuleHandlerService.PAGE_NAME + routeParams, options);
+ },
+ };
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async getMainComponent(): Promise | undefined> {
+ return AddonModFolderIndexComponent;
+ }
+
+}
+export const AddonModFolderModuleHandler = makeSingleton(AddonModFolderModuleHandlerService);
diff --git a/src/addons/mod/folder/services/handlers/pluginfile.ts b/src/addons/mod/folder/services/handlers/pluginfile.ts
new file mode 100644
index 000000000..19325811f
--- /dev/null
+++ b/src/addons/mod/folder/services/handlers/pluginfile.ts
@@ -0,0 +1,55 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CorePluginFileHandler } from '@services/plugin-file-delegate';
+import { makeSingleton } from '@singletons';
+
+/**
+ * Handler to treat links to folder.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderPluginFileHandlerService implements CorePluginFileHandler {
+
+ name = 'AddonModFolderPluginFileHandler';
+ component = 'mod_folder';
+
+ /**
+ * @inheritdoc
+ */
+ getComponentRevisionRegExp(args: string[]): RegExp | undefined {
+ // Check filearea.
+ if (args[2] == 'content') {
+ // Component + Filearea + Revision
+ return new RegExp('/mod_folder/content/([0-9]+)/');
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ getComponentRevisionReplace(): string {
+ // Component + Filearea + Revision
+ return '/mod_folder/content/0/';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+}
+export const AddonModFolderPluginFileHandler = makeSingleton(AddonModFolderPluginFileHandlerService);
diff --git a/src/addons/mod/folder/services/handlers/prefetch.ts b/src/addons/mod/folder/services/handlers/prefetch.ts
new file mode 100644
index 000000000..5c2fd9bd6
--- /dev/null
+++ b/src/addons/mod/folder/services/handlers/prefetch.ts
@@ -0,0 +1,66 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
+import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
+import { makeSingleton } from '@singletons';
+import { AddonModFolder, AddonModFolderProvider } from '../folder';
+
+/**
+ * Handler to prefetch folders.
+ */
+@Injectable({ providedIn: 'root' })
+export class AddonModFolderPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
+
+ name = 'AddonModFolder';
+ modName = 'folder';
+ component = AddonModFolderProvider.COMPONENT;
+
+ /**
+ * @inheritdoc
+ */
+ async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise {
+ const promises: Promise[] = [];
+
+ promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
+
+ if (AddonModFolder.isGetFolderWSAvailable()) {
+ promises.push(AddonModFolder.getFolder(courseId, module.id));
+ }
+
+ await Promise.all(promises);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async invalidateContent(moduleId: number, courseId: number): Promise {
+ return AddonModFolder.invalidateContent(moduleId, courseId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise {
+ const promises: Promise[] = [];
+
+ promises.push(AddonModFolder.invalidateFolderData(courseId));
+ promises.push(CoreCourse.invalidateModule(module.id));
+
+ await Promise.all(promises);
+ }
+
+}
+export const AddonModFolderPrefetchHandler = makeSingleton(AddonModFolderPrefetchHandlerService);
diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts
index e0d3f1ab6..208df1220 100644
--- a/src/addons/mod/mod.module.ts
+++ b/src/addons/mod/mod.module.ts
@@ -16,6 +16,7 @@ import { NgModule } from '@angular/core';
import { AddonModAssignModule } from './assign/assign.module';
import { AddonModBookModule } from './book/book.module';
+import { AddonModFolderModule } from './folder/folder.module';
import { AddonModForumModule } from './forum/forum.module';
import { AddonModLabelModule } from './label/label.module';
import { AddonModLessonModule } from './lesson/lesson.module';
@@ -32,6 +33,7 @@ import { AddonModQuizModule } from './quiz/quiz.module';
AddonModPageModule,
AddonModQuizModule,
AddonModLabelModule,
+ AddonModFolderModule,
],
providers: [],
exports: [],
diff --git a/src/addons/privatefiles/pages/index/index.html b/src/addons/privatefiles/pages/index/index.html
index 435540fae..9ac22822a 100644
--- a/src/addons/privatefiles/pages/index/index.html
+++ b/src/addons/privatefiles/pages/index/index.html
@@ -42,7 +42,7 @@
-
+