diff --git a/scripts/langindex.json b/scripts/langindex.json index 1e1606b32..0833ae5d8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -228,12 +228,6 @@ "addon.coursecompletion.requirement": "block_completionstatus", "addon.coursecompletion.status": "moodle", "addon.coursecompletion.viewcoursereport": "completion", - "addon.files.couldnotloadfiles": "local_moodlemobileapp", - "addon.files.emptyfilelist": "local_moodlemobileapp", - "addon.files.erroruploadnotworking": "local_moodlemobileapp", - "addon.files.files": "moodle", - "addon.files.privatefiles": "moodle", - "addon.files.sitefiles": "moodle", "addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp", "addon.messages.acceptandaddcontact": "message", "addon.messages.addcontact": "message", @@ -1049,6 +1043,12 @@ "addon.notifications.notifications": "local_moodlemobileapp", "addon.notifications.playsound": "local_moodlemobileapp", "addon.notifications.therearentnotificationsyet": "local_moodlemobileapp", + "addon.privatefiles.couldnotloadfiles": "local_moodlemobileapp", + "addon.privatefiles.emptyfilelist": "local_moodlemobileapp", + "addon.privatefiles.erroruploadnotworking": "local_moodlemobileapp", + "addon.privatefiles.files": "moodle", + "addon.privatefiles.privatefiles": "moodle", + "addon.privatefiles.sitefiles": "moodle", "addon.storagemanager.deletecourse": "local_moodlemobileapp", "addon.storagemanager.deletecourses": "local_moodlemobileapp", "addon.storagemanager.deletedatafrom": "local_moodlemobileapp", diff --git a/src/app/addon/privatefiles/lang/en.json b/src/app/addon/privatefiles/lang/en.json new file mode 100644 index 000000000..b923f6141 --- /dev/null +++ b/src/app/addon/privatefiles/lang/en.json @@ -0,0 +1,8 @@ +{ + "couldnotloadfiles": "The list of files could not be loaded.", + "emptyfilelist": "There are no files to show.", + "erroruploadnotworking": "Unfortunately it is currently not possible to upload files to your site.", + "files": "Files", + "privatefiles": "Private files", + "sitefiles": "Site files" +} \ No newline at end of file diff --git a/src/app/addon/privatefiles/pages/index/index.html b/src/app/addon/privatefiles/pages/index/index.html new file mode 100644 index 000000000..e7aa95a36 --- /dev/null +++ b/src/app/addon/privatefiles/pages/index/index.html @@ -0,0 +1,53 @@ + + + + + + {{ title }} + + + + + + + + + +
+ + {{ 'addon.privatefiles.privatefiles' | translate }} + {{ 'addon.privatefiles.sitefiles' | translate }} + +
+ + +

+ {{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }} +

+ + + + + + + + + {{file.filename}} + + + + + + + + +
+ + + + + + + +
\ No newline at end of file diff --git a/src/app/addon/privatefiles/pages/index/index.page.module.ts b/src/app/addon/privatefiles/pages/index/index.page.module.ts new file mode 100644 index 000000000..2c4e04763 --- /dev/null +++ b/src/app/addon/privatefiles/pages/index/index.page.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonPrivateFilesIndexPage } from './index.page'; + +const routes: Routes = [ + { + path: '', + component: AddonPrivateFilesIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + declarations: [ + AddonPrivateFilesIndexPage, + ], + exports: [RouterModule], +}) +export class AddonPrivateFilesIndexPageModule {} diff --git a/src/app/addon/privatefiles/pages/index/index.page.ts b/src/app/addon/privatefiles/pages/index/index.page.ts new file mode 100644 index 000000000..ca20a6df2 --- /dev/null +++ b/src/app/addon/privatefiles/pages/index/index.page.ts @@ -0,0 +1,270 @@ +// (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, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IonRefresher, NavController } from '@ionic/angular'; +import { Md5 } from 'ts-md5/dist/md5'; + +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { Translate } from '@singletons/core.singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { + AddonPrivateFiles, + AddonPrivateFilesProvider, + AddonPrivateFilesFile, + AddonPrivateFilesGetUserInfoWSResult, + AddonPrivateFilesGetFilesWSParams, +} from '@addon/privatefiles/services/privatefiles'; +import { AddonPrivateFilesHelper } from '@addon/privatefiles/services/privatefiles.helper'; +import { CoreUtils } from '@/app/services/utils/utils'; + +/** + * Page that displays the list of files. + */ +@Component({ + selector: 'page-addon-privatefiles-index', + templateUrl: 'index.html', +}) +export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { + + title!: string; // Page title. + root?: 'my' | 'site'; // The root of the files loaded: 'my' or 'site'. + path?: AddonPrivateFilesGetFilesWSParams; // The path of the directory being loaded. If empty path it means load the root. + showPrivateFiles!: boolean; // Whether the user can view private files. + showSiteFiles!: boolean; // Whether the user can view site files. + showUpload!: boolean; // Whether the user can upload files. + userQuota?: number; // The user quota (in bytes). + filesInfo?: AddonPrivateFilesGetUserInfoWSResult; // Info about private files (size, number of files, etc.). + spaceUsed?: string; // Space used in a readable format. + userQuotaReadable?: string; // User quota in a readable format. + files?: AddonPrivateFilesFile[]; // List of files. + component!: string; // Component to link the file downloads to. + filesLoaded = false; // Whether the files are loaded. + + protected updateSiteObserver: CoreEventObserver; + + constructor( + protected route: ActivatedRoute, + protected navCtrl: NavController, + ) { + // Update visibility if current site info is updated. + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.setVisibility(); + }, CoreSites.instance.getCurrentSiteId()); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.root = this.route.snapshot.queryParams['root']; + + if (this.route.snapshot.queryParams['contextid']) { + // Loading a certain folder. + this.path = { + contextid: this.route.snapshot.queryParams['contextid'], + component: this.route.snapshot.queryParams['component'], + filearea: this.route.snapshot.queryParams['filearea'], + itemid: this.route.snapshot.queryParams['itemid'], + filepath: this.route.snapshot.queryParams['filepath'], + filename: this.route.snapshot.queryParams['filename'], + }; + } + + this.title = this.path?.filename || Translate.instance.instant('addon.privatefiles.files'); + + this.setVisibility(); + this.userQuota = CoreSites.instance.getCurrentSite()?.getInfo()?.userquota; + + if (!this.root) { + // Load private files by default. + if (this.showPrivateFiles) { + this.root = 'my'; + } else if (this.showSiteFiles) { + this.root = 'site'; + } + } + + if (this.root) { + this.rootChanged(); + } else { + this.filesLoaded = true; + } + } + + /** + * Set visibility of some items based on site data. + */ + protected setVisibility(): void { + this.showPrivateFiles = AddonPrivateFiles.instance.canViewPrivateFiles(); + this.showSiteFiles = AddonPrivateFiles.instance.canViewSiteFiles(); + this.showUpload = AddonPrivateFiles.instance.canUploadFiles(); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + refreshData(event?: CustomEvent): void { + this.refreshFiles().finally(() => { + event?.detail.complete(); + }); + } + + /** + * Function called when the root has changed. + */ + rootChanged(): void { + this.filesLoaded = false; + this.component = this.root == 'my' ? AddonPrivateFilesProvider.PRIVATE_FILES_COMPONENT : + AddonPrivateFilesProvider.SITE_FILES_COMPONENT; + + this.fetchFiles().finally(() => { + this.filesLoaded = true; + }); + } + + /** + * Upload a new file. + */ + async uploadFile(): Promise { + const canUpload = await AddonPrivateFiles.instance.versionCanUploadFiles(); + + if (!canUpload) { + CoreDomUtils.instance.showAlertTranslated('core.notice', 'addon.privatefiles.erroruploadnotworking'); + + return; + } + + if (!CoreApp.instance.isOnline()) { + CoreDomUtils.instance.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true); + + return; + } + + try { + await AddonPrivateFilesHelper.instance.uploadPrivateFile(this.filesInfo); + + // File uploaded, refresh the list. + this.filesLoaded = false; + + await CoreUtils.instance.ignoreErrors(this.refreshFiles()); + + this.filesLoaded = true; + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.fileuploader.errorwhileuploading', true); + } + } + + /** + * Fetch the files. + * + * @return Promise resolved when done. + */ + protected async fetchFiles(): Promise { + try { + if (this.path) { + // Path is set, serve the files the user requested. + this.files = await AddonPrivateFiles.instance.getFiles(this.path); + + return; + } + + // The path is unknown, the user must be requesting a root. + if (this.root == 'site') { + this.title = Translate.instance.instant('addon.privatefiles.sitefiles'); + + this.files = await AddonPrivateFiles.instance.getSiteFiles(); + } else if (this.root == 'my') { + this.title = Translate.instance.instant('addon.privatefiles.files'); + + this.files = await AddonPrivateFiles.instance.getPrivateFiles(); + + if (this.showUpload && AddonPrivateFiles.instance.canGetPrivateFilesInfo() && this.userQuota && + this.userQuota > 0) { + // Get the info to calculate the available size. + this.filesInfo = await AddonPrivateFiles.instance.getPrivateFilesInfo(); + + this.spaceUsed = CoreTextUtils.instance.bytesToSize(this.filesInfo.filesizewithoutreferences, 1); + this.userQuotaReadable = CoreTextUtils.instance.bytesToSize(this.userQuota, 1); + } else { + // User quota isn't useful, delete it. + delete this.userQuota; + } + } else { + // Unknown root. + CoreDomUtils.instance.showErrorModal('addon.privatefiles.couldnotloadfiles', true); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'addon.privatefiles.couldnotloadfiles', true); + } + } + + /** + * Refresh the displayed files. + * + * @return Promise resolved when done. + */ + protected async refreshFiles(): Promise { + try { + await Promise.all([ + AddonPrivateFiles.instance.invalidateDirectory(this.root, this.path), + AddonPrivateFiles.instance.invalidatePrivateFilesInfoForUser(), + ]); + } finally { + await this.fetchFiles(); + } + } + + /** + * Open a folder. + * + * @param folder Folder to open. + */ + openFolder(folder: AddonPrivateFilesFile): void { + const params = { + contextid: folder.contextid, + component: folder.component || '', + filearea: folder.filearea || '', + itemid: folder.itemid || 0, + filepath: folder.filepath || '', + filename: folder.filename || '', + }; + + if (folder.component) { + // Delete unused elements that may break the request. + params.filename = ''; + } + + const hash = Md5.hashAsciiStr(JSON.stringify(params)); + + this.navCtrl.navigateForward([`../${hash}`], { + relativeTo: this.route, + queryParams: params, + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.updateSiteObserver?.off(); + } + +} diff --git a/src/app/addon/privatefiles/privatefiles-init.module.ts b/src/app/addon/privatefiles/privatefiles-init.module.ts new file mode 100644 index 000000000..b09af1fd2 --- /dev/null +++ b/src/app/addon/privatefiles/privatefiles-init.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 { Routes } from '@angular/router'; + +import { CoreMainMenuDelegate } from '@core/mainmenu/services/mainmenu.delegate'; +import { CoreMainMenuRoutingModule } from '@core/mainmenu/mainmenu-routing.module'; +import { AddonPrivateFilesMainMenuHandler } from './services/handlers/mainmenu'; + +const routes: Routes = [ + { + path: 'addon-privatefiles', + loadChildren: () => import('@addon/privatefiles/privatefiles.module').then(m => m.AddonPrivateFilesModule), + }, +]; + +@NgModule({ + imports: [CoreMainMenuRoutingModule.forChild(routes)], + exports: [CoreMainMenuRoutingModule], + providers: [ + AddonPrivateFilesMainMenuHandler, + ], +}) +export class AddonPrivateFilesInitModule { + + constructor( + mainMenuDelegate: CoreMainMenuDelegate, + mainMenuHandler: AddonPrivateFilesMainMenuHandler, + ) { + mainMenuDelegate.registerHandler(mainMenuHandler); + } + +} diff --git a/src/app/addon/privatefiles/privatefiles-routing.module.ts b/src/app/addon/privatefiles/privatefiles-routing.module.ts new file mode 100644 index 000000000..97167e019 --- /dev/null +++ b/src/app/addon/privatefiles/privatefiles-routing.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 { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'root', // Fake "hash". + pathMatch: 'full', + }, + { + path: ':hash', + loadChildren: () => import('./pages/index/index.page.module').then( m => m.AddonPrivateFilesIndexPageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AddonPrivateFilesRoutingModule {} diff --git a/src/app/addon/privatefiles/privatefiles.module.ts b/src/app/addon/privatefiles/privatefiles.module.ts new file mode 100644 index 000000000..3ddb6e41c --- /dev/null +++ b/src/app/addon/privatefiles/privatefiles.module.ts @@ -0,0 +1,25 @@ +// (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 { AddonPrivateFilesRoutingModule } from './privatefiles-routing.module'; + +@NgModule({ + imports: [ + AddonPrivateFilesRoutingModule, + ], + declarations: [], +}) +export class AddonPrivateFilesModule {} diff --git a/src/app/addon/privatefiles/services/handlers/mainmenu.ts b/src/app/addon/privatefiles/services/handlers/mainmenu.ts new file mode 100644 index 000000000..a858b611e --- /dev/null +++ b/src/app/addon/privatefiles/services/handlers/mainmenu.ts @@ -0,0 +1,52 @@ +// (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 { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/services/mainmenu.delegate'; +import { AddonPrivateFiles } from '@addon/privatefiles/services/privatefiles'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class AddonPrivateFilesMainMenuHandler implements CoreMainMenuHandler { + + name = 'AddonPrivateFiles'; + priority = 400; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + async isEnabled(): Promise { + return AddonPrivateFiles.instance.isPluginEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'fa-folder', + title: 'addon.privatefiles.files', + page: 'addon-privatefiles', + class: 'addon-privatefiles-handler', + }; + } + +} diff --git a/src/app/addon/privatefiles/services/privatefiles.helper.ts b/src/app/addon/privatefiles/services/privatefiles.helper.ts new file mode 100644 index 000000000..2dbd3e5d9 --- /dev/null +++ b/src/app/addon/privatefiles/services/privatefiles.helper.ts @@ -0,0 +1,77 @@ +// (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 { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreFileUploaderHelper } from '@core/fileuploader/services/fileuploader.helper'; +import { AddonPrivateFiles, AddonPrivateFilesGetUserInfoWSResult } from './privatefiles'; +import { CoreError } from '@classes/errors/error'; +import { makeSingleton, Translate } from '@singletons/core.singletons'; + +/** + * Service that provides some helper functions regarding private and site files. + */ +@Injectable({ + providedIn: 'root', +}) +export class AddonPrivateFilesHelperProvider { + + /** + * Select a file, upload it and move it to private files. + * + * @param info Private files info. See AddonPrivateFilesProvider.getPrivateFilesInfo. + * @return Promise resolved when a file is uploaded, rejected otherwise. + */ + async uploadPrivateFile(info?: AddonPrivateFilesGetUserInfoWSResult): Promise { + // Calculate the max size. + const currentSite = CoreSites.instance.getCurrentSite(); + let maxSize = currentSite?.getInfo()?.usermaxuploadfilesize || -1; + let userQuota = currentSite?.getInfo()?.userquota; + + if (userQuota === 0) { + // 0 means ignore user quota. In the app it is -1. + userQuota = -1; + } else if (userQuota !== undefined && userQuota > 0 && info !== undefined) { + userQuota = userQuota - info.filesizewithoutreferences; + } + + if (userQuota !== undefined) { + // Use the minimum value. + maxSize = Math.min(maxSize, userQuota); + } + + // Select and upload the file. + const result = await CoreFileUploaderHelper.instance.selectAndUploadFile(maxSize); + + if (!result) { + throw new CoreError(Translate.instance.instant('core.fileuploader.errorwhileuploading')); + } + + // File uploaded. Move it to private files. + const modal = await CoreDomUtils.instance.showModalLoading('core.fileuploader.uploading', true); + + try { + await AddonPrivateFiles.instance.moveFromDraftToPrivate(result.itemid); + + CoreDomUtils.instance.showToast('core.fileuploader.fileuploaded', true, undefined, 'core-toast-success'); + } finally { + modal.dismiss(); + } + } + +} + +export class AddonPrivateFilesHelper extends makeSingleton(AddonPrivateFilesHelperProvider) {} diff --git a/src/app/addon/privatefiles/services/privatefiles.ts b/src/app/addon/privatefiles/services/privatefiles.ts new file mode 100644 index 000000000..e816bbd09 --- /dev/null +++ b/src/app/addon/privatefiles/services/privatefiles.ts @@ -0,0 +1,497 @@ +// (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 { CoreSites } from '@services/sites'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreWSExternalWarning } from '@services/ws'; +import { CoreSite } from '@classes/site'; +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Service to handle my files and site files. + */ +@Injectable({ + providedIn: 'root', +}) +export class AddonPrivateFilesProvider { + + // Keep old names for backwards compatibility. + static readonly PRIVATE_FILES_COMPONENT = 'mmaFilesMy'; + static readonly SITE_FILES_COMPONENT = 'mmaFilesSite'; + + protected readonly ROOT_CACHE_KEY = 'mmaFiles:'; + + /** + * Check if core_user_get_private_files_info WS call is available. + * + * @return Whether the WS is available, false otherwise. + */ + canGetPrivateFilesInfo(): boolean { + return CoreSites.instance.wsAvailableInCurrentSite('core_user_get_private_files_info'); + } + + /** + * Check if user can view his private files. + * + * @return Whether the user can view his private files. + */ + canViewPrivateFiles(): boolean { + const currentSite = CoreSites.instance.getCurrentSite(); + if (!currentSite) { + return false; + } + + return currentSite.canAccessMyFiles() && !this.isPrivateFilesDisabledInSite(); + } + + /** + * Check if user can view site files. + * + * @return Whether the user can view site files. + */ + canViewSiteFiles(): boolean { + return !this.isSiteFilesDisabledInSite(); + } + + /** + * Check if user can upload private files. + * + * @return Whether the user can upload private files. + */ + canUploadFiles(): boolean { + const currentSite = CoreSites.instance.getCurrentSite(); + if (!currentSite) { + return false; + } + + return currentSite.canAccessMyFiles() && currentSite.canUploadFiles() && !this.isUploadDisabledInSite(); + } + + /** + * Get the list of files. + * + * @param params A list of parameters accepted by the Web service. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the files. + */ + async getFiles(params: AddonPrivateFilesGetFilesWSParams, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + + const preSets = { + cacheKey: this.getFilesListCacheKey(params), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + const result: AddonPrivateFilesGetFilesWSResult = await site.read('core_files_get_files', params, preSets); + + if (!result.files) { + return []; + } + + return result.files.map((entry) => { + entry.fileurl = entry.url; + + if (entry.isdir) { + entry.imgPath = CoreMimetypeUtils.instance.getFolderIcon(); + } else { + entry.imgPath = CoreMimetypeUtils.instance.getFileIcon(entry.filename); + } + + return entry; + }); + + } + + /** + * Get cache key for file list WS calls. + * + * @param params Params of the WS. + * @return Cache key. + */ + protected getFilesListCacheKey(params: AddonPrivateFilesGetFilesWSParams): string { + const root = !params.component ? 'site' : 'my'; + + return this.ROOT_CACHE_KEY + 'list:' + root + ':' + params.contextid + ':' + params.filepath; + } + + /** + * Get the private files of the current user. + * + * @return Promise resolved with the files. + */ + getPrivateFiles(): Promise { + return this.getFiles(this.getPrivateFilesRootParams()); + } + + /** + * Get params to get root private files directory. + * + * @return Params. + */ + protected getPrivateFilesRootParams(): AddonPrivateFilesGetFilesWSParams { + return { + contextid: -1, + component: 'user', + filearea: 'private', + contextlevel: 'user', + instanceid: CoreSites.instance.getCurrentSite()?.getUserId(), + itemid: 0, + filepath: '', + filename: '', + }; + } + + /** + * Get private files info. + * + * @param userId User ID. If not defined, current user in the site. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with the info. + */ + async getPrivateFilesInfo(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + userId = userId || site.getUserId(); + + const params: AddonPrivateFilesGetUserInfoWSParams = { + userid: userId, + }; + const preSets = { + cacheKey: this.getPrivateFilesInfoCacheKey(userId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + return site.read('core_user_get_private_files_info', params, preSets); + } + + /** + * Get the cache key for private files info WS calls. + * + * @param userId User ID. + * @return Cache key. + */ + protected getPrivateFilesInfoCacheKey(userId: number): string { + return this.getPrivateFilesInfoCommonCacheKey() + ':' + userId; + } + + /** + * Get the common part of the cache keys for private files info WS calls. + * + * @return Cache key. + */ + protected getPrivateFilesInfoCommonCacheKey(): string { + return this.ROOT_CACHE_KEY + 'privateInfo'; + } + + /** + * Get the site files. + * + * @return Promise resolved with the files. + */ + getSiteFiles(): Promise { + return this.getFiles(this.getSiteFilesRootParams()); + } + + /** + * Get params to get root site files directory. + * + * @return Params. + */ + protected getSiteFilesRootParams(): AddonPrivateFilesGetFilesWSParams { + return { + contextid: 0, + component: '', + filearea: '', + itemid: 0, + filepath: '', + filename: '', + }; + } + + /** + * Invalidates list of files in a certain directory. + * + * @param root Root of the directory ('my' for private files, 'site' for site files). + * @param params Params to the directory. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateDirectory(root?: 'my' | 'site', params?: AddonPrivateFilesGetFilesWSParams, siteId?: string): Promise { + if (!root) { + return; + } + + if (!params) { + if (root === 'site') { + params = this.getSiteFilesRootParams(); + } else { + params = this.getPrivateFilesRootParams(); + } + } + + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getFilesListCacheKey(params)); + } + + /** + * Invalidates private files info for all users. + * + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidatePrivateFilesInfo(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getPrivateFilesInfoCommonCacheKey()); + } + + /** + * Invalidates private files info for a certain user. + * + * @param userId User ID. If not defined, current user in the site. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidatePrivateFilesInfoForUser(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getPrivateFilesInfoCacheKey(userId || site.getUserId())); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isDisabledInSite(site); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isDisabledInSite(site: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return site.isFeatureDisabled('CoreMainMenuDelegate_AddonPrivateFiles'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @return True if enabled, false otherwise. + */ + isPluginEnabled(): boolean { + return this.canViewPrivateFiles() || this.canViewSiteFiles() || this.canUploadFiles(); + } + + /** + * Check if private files is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isPrivateFilesDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isPrivateFilesDisabledInSite(site); + } + + /** + * Check if private files is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isPrivateFilesDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.isFeatureDisabled('AddonPrivateFilesPrivateFiles'); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isSiteFilesDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isSiteFilesDisabledInSite(site); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isSiteFilesDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.isFeatureDisabled('AddonPrivateFilesSiteFiles'); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isUploadDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isUploadDisabledInSite(site); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isUploadDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.isFeatureDisabled('AddonPrivateFilesUpload'); + } + + /** + * Move a file from draft area to private files. + * + * @param draftId The draft area ID of the file. + * @param siteid ID of the site. If not defined, use current site. + * @return Promise resolved in success, rejected otherwise. + */ + async moveFromDraftToPrivate(draftId: number, siteId?: string): Promise { + const params: AddonPrivateFilesAddUserPrivateFilesWSParams = { + draftid: draftId, + }; + const preSets = { + responseExpected: false, + }; + + const site = await CoreSites.instance.getSite(siteId); + + return site.write('core_user_add_user_private_files', params, preSets); + } + + /** + * Check the Moodle version in order to check if upload files is working. + * + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with true if WS is working, false otherwise. + */ + async versionCanUploadFiles(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + // Upload private files doesn't work for Moodle 3.1.0 due to a bug. + return site.isVersionGreaterEqualThan('3.1.1'); + } + +} + +export class AddonPrivateFiles extends makeSingleton(AddonPrivateFilesProvider) {} + +/** + * File data returned by core_files_get_files. + */ +export type AddonPrivateFilesFile = { + contextid: number; + component: string; + filearea: string; + itemid: number; + filepath: string; + filename: string; + isdir: boolean; + url: string; + timemodified: number; + timecreated?: number; // Time created. + filesize?: number; // File size. + author?: string; // File owner. + license?: string; // File license. +} & AddonPrivateFilesFileCalculatedData; + +/** + * Calculated data for AddonPrivateFilesFile. + */ +export type AddonPrivateFilesFileCalculatedData = { + fileurl: string; // File URL, using same name as CoreWSExternalFile. + imgPath?: string; // Path to file icon's image. +}; +/** + * Params of WS core_files_get_files. + */ +export type AddonPrivateFilesGetFilesWSParams = { + contextid: number; // Context id Set to -1 to use contextlevel and instanceid. + component: string; // Component. + filearea: string; // File area. + itemid: number; // Associated id. + filepath: string; // File path. + filename: string; // File name. + modified?: number; // Timestamp to return files changed after this time. + contextlevel?: string; // The context level for the file location. + instanceid?: number; // The instance id for where the file is located. +}; + +/** + * Result of WS core_files_get_files. + */ +export type AddonPrivateFilesGetFilesWSResult = { + parents: { + contextid: number; + component: string; + filearea: string; + itemid: number; + filepath: string; + filename: string; + }[]; + files: AddonPrivateFilesFile[]; +}; + +/** + * Params of core_user_get_private_files_info WS. + */ +export type AddonPrivateFilesGetUserInfoWSParams = { + userid?: number; // Id of the user, default to current user. +}; + +/** + * Data returned by core_user_get_private_files_info WS. + */ +export type AddonPrivateFilesGetUserInfoWSResult = { + filecount: number; // Number of files in the area. + foldercount: number; // Number of folders in the area. + filesize: number; // Total size of the files in the area. + filesizewithoutreferences: number; // Total size of the area excluding file references. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of core_user_add_user_private_files WS. + */ +export type AddonPrivateFilesAddUserPrivateFilesWSParams = { + draftid: number; // Draft area id. +}; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1465a4fa9..953bc83ef 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -61,6 +61,10 @@ import { CoreEmulatorModule } from '@core/emulator/emulator.module'; import { CoreLoginModule } from '@core/login/login.module'; import { CoreCoursesModule } from '@core/courses/courses.module'; import { CoreSettingsInitModule } from '@core/settings/settings-init.module'; +import { CoreFileUploaderInitModule } from '@core/fileuploader/fileuploader-init.module'; + +// Import addons init modules. +import { AddonPrivateFilesInitModule } from '@addon/privatefiles/privatefiles-init.module'; import { setSingletonsInjector } from '@singletons/core.singletons'; @@ -91,6 +95,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreLoginModule, CoreCoursesModule, CoreSettingsInitModule, + CoreFileUploaderInitModule, + AddonPrivateFilesInitModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/src/app/core/login/pages/sites/sites.html b/src/app/core/login/pages/sites/sites.html index 726b56252..bd544d273 100644 --- a/src/app/core/login/pages/sites/sites.html +++ b/src/app/core/login/pages/sites/sites.html @@ -33,12 +33,12 @@ - - - - - - - - + + + + + + + + diff --git a/src/app/services/utils/text.ts b/src/app/services/utils/text.ts index 2a3984044..cb96dd0e9 100644 --- a/src/app/services/utils/text.ts +++ b/src/app/services/utils/text.ts @@ -54,14 +54,14 @@ export class CoreTextUtilsProvider { { old: /_mmaNotifications/g, new: '_AddonNotifications' }, { old: /_mmaMessages/g, new: '_AddonMessages' }, { old: /_mmaCalendar/g, new: '_AddonCalendar' }, - { old: /_mmaFiles/g, new: '_AddonFiles' }, + { old: /_mmaFiles/g, new: '_AddonPrivateFiles' }, { old: /_mmaParticipants/g, new: '_CoreUserParticipants' }, { old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' }, { old: /_mmaNotes/g, new: '_AddonNotes' }, { old: /_mmaBadges/g, new: '_AddonBadges' }, - { old: /files_privatefiles/g, new: 'AddonFilesPrivateFiles' }, - { old: /files_sitefiles/g, new: 'AddonFilesSiteFiles' }, - { old: /files_upload/g, new: 'AddonFilesUpload' }, + { old: /files_privatefiles/g, new: 'AddonPrivateFilesPrivateFiles' }, + { old: /files_sitefiles/g, new: 'AddonPrivateFilesSiteFiles' }, + { old: /files_upload/g, new: 'AddonPrivateFilesUpload' }, { old: /_mmaModAssign/g, new: '_AddonModAssign' }, { old: /_mmaModBook/g, new: '_AddonModBook' }, { old: /_mmaModChat/g, new: '_AddonModChat' }, diff --git a/src/app/singletons/core.singletons.ts b/src/app/singletons/core.singletons.ts index 561a8dd98..61c539be1 100644 --- a/src/app/singletons/core.singletons.ts +++ b/src/app/singletons/core.singletons.ts @@ -22,8 +22,11 @@ import { ModalController as ModalControllerService, ToastController as ToastControllerService, GestureController as GestureControllerService, + ActionSheetController as ActionSheetControllerService, } from '@ionic/angular'; +import { Camera as CameraService } from '@ionic-native/camera/ngx'; +import { Chooser as ChooserService } from '@ionic-native/chooser/ngx'; import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx'; import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx'; import { Device as DeviceService } from '@ionic-native/device/ngx'; @@ -36,6 +39,8 @@ import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browse import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx'; import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx'; import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx'; +import { Media as MediaService } from '@ionic-native/media/ngx'; +import { MediaCapture as MediaCaptureService } from '@ionic-native/media-capture/ngx'; import { Network as NetworkService } from '@ionic-native/network/ngx'; import { Push as PushService } from '@ionic-native/push/ngx'; import { QRScanner as QRScannerService } from '@ionic-native/qr-scanner/ngx'; @@ -71,6 +76,8 @@ export function makeSingleton(injectionToken: CoreInjectionToken