From 55393cd1ee1d0c64ddf037791e47eb43549cb22e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 23 Jan 2018 15:29:00 +0100 Subject: [PATCH 1/3] MOBILE-2322 files: Create providers and handler --- src/addon/files/files.module.ts | 36 ++ src/addon/files/lang/en.json | 8 + src/addon/files/providers/files.ts | 415 ++++++++++++++++++ src/addon/files/providers/helper.ts | 74 ++++ src/addon/files/providers/mainmenu-handler.ts | 51 +++ src/app/app.module.ts | 4 +- 6 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 src/addon/files/files.module.ts create mode 100644 src/addon/files/lang/en.json create mode 100644 src/addon/files/providers/files.ts create mode 100644 src/addon/files/providers/helper.ts create mode 100644 src/addon/files/providers/mainmenu-handler.ts diff --git a/src/addon/files/files.module.ts b/src/addon/files/files.module.ts new file mode 100644 index 000000000..56560abc2 --- /dev/null +++ b/src/addon/files/files.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { AddonFilesProvider } from './providers/files'; +import { AddonFilesHelperProvider } from './providers/helper'; +import { AddonFilesMainMenuHandler } from './providers/mainmenu-handler'; +import { CoreMainMenuDelegate } from '../../core/mainmenu/providers/delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonFilesProvider, + AddonFilesHelperProvider, + AddonFilesMainMenuHandler + ] +}) +export class AddonFilesModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, filesHandler: AddonFilesMainMenuHandler) { + mainMenuDelegate.registerHandler(filesHandler); + } +} diff --git a/src/addon/files/lang/en.json b/src/addon/files/lang/en.json new file mode 100644 index 000000000..6f0e863a2 --- /dev/null +++ b/src/addon/files/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": "My files", + "privatefiles": "Private files", + "sitefiles": "Site files" +} \ No newline at end of file diff --git a/src/addon/files/providers/files.ts b/src/addon/files/providers/files.ts new file mode 100644 index 000000000..268a835a3 --- /dev/null +++ b/src/addon/files/providers/files.ts @@ -0,0 +1,415 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider } from '../../../providers/sites'; +import { CoreMimetypeUtilsProvider } from '../../../providers/utils/mimetype'; +import { CoreSite } from '../../../classes/site'; +import { Md5 } from 'ts-md5/dist/md5'; + +/** + * Service to handle my files and site files. + */ +@Injectable() +export class AddonFilesProvider { + protected ROOT_CACHE_KEY = 'mmaFiles:'; + static PRIVATE_FILES_COMPONENT = 'mmaFilesMy'; + static SITE_FILES_COMPONENT = 'mmaFilesSite'; + + constructor(private sitesProvider: CoreSitesProvider, private mimeUtils: CoreMimetypeUtilsProvider) { } + + /** + * Check if core_user_get_private_files_info WS call is available. + * + * @return {boolean} Whether the WS is available, false otherwise. + */ + canGetPrivateFilesInfo(): boolean { + return this.sitesProvider.getCurrentSite().wsAvailable('core_user_get_private_files_info'); + } + + /** + * Check if user can view his private files. + * + * @return {boolean} Whether the user can view his private files. + */ + canViewPrivateFiles(): boolean { + return this.sitesProvider.getCurrentSite().canAccessMyFiles() && !this.isPrivateFilesDisabledInSite(); + } + + /** + * Check if user can view site files. + * + * @return {boolean} Whether the user can view site files. + */ + canViewSiteFiles(): boolean { + return !this.isSiteFilesDisabledInSite(); + } + + /** + * Check if user can upload private files. + * + * @return {boolean} Whether the user can upload private files. + */ + canUploadFiles(): boolean { + const currentSite = this.sitesProvider.getCurrentSite(); + + return currentSite.canAccessMyFiles() && currentSite.canUploadFiles() && !this.isUploadDisabledInSite(); + } + + /** + * Get the list of files. + * + * @param {any} params A list of parameters accepted by the Web service. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getFiles(params: any, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + const preSets = { + cacheKey: this.getFilesListCacheKey(params) + }; + + return site.read('core_files_get_files', params, preSets); + }).then((result) => { + const entries = []; + + if (result.files) { + result.files.forEach((entry) => { + if (entry.isdir) { + // Create a "link" to load the folder. + entry.link = { + contextid: entry.contextid || '', + component: entry.component || '', + filearea: entry.filearea || '', + itemid: entry.itemid || 0, + filepath: entry.filepath || '', + filename: entry.filename || '' + }; + + if (entry.component) { + // Delete unused elements that may break the request. + entry.link.filename = ''; + } + } + + if (entry.isdir) { + entry.imgPath = this.mimeUtils.getFolderIcon(); + } else { + entry.imgPath = this.mimeUtils.getFileIcon(entry.filename); + } + + entries.push(entry); + }); + } + + return entries; + }); + } + + /** + * Get cache key for file list WS calls. + * + * @param {any} params Params of the WS. + * @return {string} Cache key. + */ + protected getFilesListCacheKey(params: any): 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} Promise resolved with the files. + */ + getPrivateFiles(): Promise { + return this.getFiles(this.getPrivateFilesRootParams()); + } + + /** + * Get params to get root private files directory. + * + * @return {any} Params. + */ + protected getPrivateFilesRootParams(): any { + return { + contextid: -1, + component: 'user', + filearea: 'private', + contextlevel: 'user', + instanceid: this.sitesProvider.getCurrentSite().getUserId(), + itemid: 0, + filepath: '', + filename: '' + }; + } + + /** + * Get private files info. + * + * @param {number} [userId] User ID. If not defined, current user in the site. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with the info. + */ + getPrivateFilesInfo(userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const params = { + userid: userId + }, + preSets = { + cacheKey: this.getPrivateFilesInfoCacheKey(userId) + }; + + return site.read('core_user_get_private_files_info', params, preSets); + }); + } + + /** + * Get the cache key for private files info WS calls. + * + * @param {number} userId User ID. + * @return {string} 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 {string} Cache key. + */ + protected getPrivateFilesInfoCommonCacheKey(): string { + return this.ROOT_CACHE_KEY + 'privateInfo'; + } + + /** + * Get the site files. + * + * @return {Promise} Promise resolved with the files. + */ + getSiteFiles(): Promise { + return this.getFiles(this.getSiteFilesRootParams()); + } + + /** + * Get params to get root site files directory. + * + * @return {any} Params. + */ + protected getSiteFilesRootParams(): any { + return { + contextid: 0, + component: '', + filearea: '', + itemid: 0, + filepath: '', + filename: '' + }; + } + + /** + * Invalidates list of files in a certain directory. + * + * @param {string} root Root of the directory ('my' for private files, 'site' for site files). + * @param {string} path Path to the directory. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDirectory(root: string, path: string, siteId?: string): Promise { + let params; + if (!path) { + if (root === 'site') { + params = this.getSiteFilesRootParams(); + } else if (root === 'my') { + params = this.getPrivateFilesRootParams(); + } + } else { + params = path; + } + + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getFilesListCacheKey(params)); + }); + } + + /** + * Invalidates private files info for all users. + * + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidatePrivateFilesInfo(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getPrivateFilesInfoCommonCacheKey()); + }); + } + + /** + * Invalidates private files info for a certain user. + * + * @param {number} [userId] User ID. If not defined, current user in the site. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidatePrivateFilesInfoForUser(userId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getPrivateFilesInfoCacheKey(userId)); + }); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isDisabledInSite(site); + }); + } + + /** + * Check if Files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isDisabledInSite(site: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('$mmSideMenuDelegate_mmaFiles'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @return {boolean} 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 {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isPrivateFilesDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isPrivateFilesDisabledInSite(site); + }); + } + + /** + * Check if private files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isPrivateFilesDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_privatefiles'); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isSiteFilesDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isSiteFilesDisabledInSite(site); + }); + } + + /** + * Check if site files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isSiteFilesDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_sitefiles'); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param {string} [siteId] Site Id. If not defined, use current site. + * @return {Promise} Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + isUploadDisabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return this.isUploadDisabledInSite(site); + }); + } + + /** + * Check if upload files is disabled in a certain site. + * + * @param {CoreSite} [site] Site. If not defined, use current site. + * @return {boolean} Whether it's disabled. + */ + isUploadDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('files_upload'); + } + + /** + * Move a file from draft area to private files. + * + * @param {number} draftId The draft area ID of the file. + * @param {string} [siteid] ID of the site. If not defined, use current site. + * @return {Promise} Promise resolved in success, rejected otherwise. + */ + moveFromDraftToPrivate(draftId: number, siteId?: string): Promise { + const params = { + draftid: draftId + }, + preSets = { + responseExpected: false + }; + + return this.sitesProvider.getSite(siteId).then((site) => { + 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 {string} [siteId] Site ID. If not defined, use current site. + * @return {Promise} Promise resolved with true if WS is working, false otherwise. + */ + versionCanUploadFiles(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Upload private files doesn't work for Moodle 3.1.0 due to a bug. + return site.isVersionGreaterEqualThan('3.1.1'); + }); + } +} diff --git a/src/addon/files/providers/helper.ts b/src/addon/files/providers/helper.ts new file mode 100644 index 000000000..467cb0ad9 --- /dev/null +++ b/src/addon/files/providers/helper.ts @@ -0,0 +1,74 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreSitesProvider } from '../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; +import { CoreFileUploaderHelperProvider } from '../../../core/fileuploader/providers/helper'; +import { AddonFilesProvider } from './files'; + +/** + * Service that provides some features regarding private and site files. + */ +@Injectable() +export class AddonFilesHelperProvider { + + constructor(private sitesProvider: CoreSitesProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider, + private filesProvider: AddonFilesProvider, private domUtils: CoreDomUtilsProvider) { } + + /** + * Select a file, upload it and move it to private files. + * + * @param {any} [info] Private files info. See AddonFilesProvider.getPrivateFilesInfo. + * @return {Promise} Promise resolved when a file is uploaded, rejected otherwise. + */ + uploadPrivateFile(info?: any): Promise { + // Calculate the max size. + const currentSite = this.sitesProvider.getCurrentSite(); + let maxSize = currentSite.getInfo().usermaxuploadfilesize, + userQuota = currentSite.getInfo().userquota; + + if (userQuota === 0) { + // 0 means ignore user quota. In the app it is -1. + userQuota = -1; + } else if (userQuota > 0 && typeof info != 'undefined') { + userQuota = userQuota - info.filesizewithoutreferences; + } + + if (typeof userQuota != 'undefined') { + // Use the minimum value. + maxSize = Math.min(maxSize, userQuota); + } + + // Select and upload the file. + return this.fileUploaderHelper.selectAndUploadFile(maxSize).then((result) => { + if (!result) { + return Promise.reject(null); + } + + // File uploaded. Move it to private files. + const modal = this.domUtils.showModalLoading('core.fileuploader.uploading', true); + + return this.filesProvider.moveFromDraftToPrivate(result.itemid).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'core.fileuploader.errorwhileuploading', true); + + return Promise.reject(null); + }).finally(() => { + modal.dismiss(); + }); + }).then(() => { + this.domUtils.showAlertTranslated('core.success', 'core.fileuploader.fileuploaded'); + }); + } +} diff --git a/src/addon/files/providers/mainmenu-handler.ts b/src/addon/files/providers/mainmenu-handler.ts new file mode 100644 index 000000000..fb7e82d71 --- /dev/null +++ b/src/addon/files/providers/mainmenu-handler.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { AddonFilesProvider } from './files'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../../core/mainmenu/providers/delegate'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class AddonFilesMainMenuHandler implements CoreMainMenuHandler { + name = 'AddonFiles'; + priority = 200; + + constructor(private filesProvider: AddonFilesProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.filesProvider.isPluginEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'folder', + title: 'addon.files.files', + page: 'AddonFilesListPage', + class: 'addon-files-handler' + }; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3d3620622..d96ef144c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -65,6 +65,7 @@ import { CoreUserModule } from '../core/user/user.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module'; +import { AddonFilesModule } from '../addon/files/files.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -101,7 +102,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreContentLinksModule, CoreUserModule, AddonCalendarModule, - AddonUserProfileFieldModule + AddonUserProfileFieldModule, + AddonFilesModule ], bootstrap: [IonicApp], entryComponents: [ From 052208dac73434da0b50ec6665b79d31d090a806 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 24 Jan 2018 11:15:54 +0100 Subject: [PATCH 2/3] MOBILE-2322 files: Implement list page --- src/addon/files/pages/list/list.html | 44 +++++ src/addon/files/pages/list/list.module.ts | 33 ++++ src/addon/files/pages/list/list.ts | 215 ++++++++++++++++++++++ src/providers/utils/utils.ts | 2 +- 4 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/addon/files/pages/list/list.html create mode 100644 src/addon/files/pages/list/list.module.ts create mode 100644 src/addon/files/pages/list/list.ts diff --git a/src/addon/files/pages/list/list.html b/src/addon/files/pages/list/list.html new file mode 100644 index 000000000..b55f063db --- /dev/null +++ b/src/addon/files/pages/list/list.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + +
+ + {{ 'addon.files.privatefiles' | translate }} + {{ 'addon.files.sitefiles' | translate }} + +
+ + +

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

+ + + + + + + + +
+ + + + + +
\ No newline at end of file diff --git a/src/addon/files/pages/list/list.module.ts b/src/addon/files/pages/list/list.module.ts new file mode 100644 index 000000000..50a40684d --- /dev/null +++ b/src/addon/files/pages/list/list.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; +import { AddonFilesListPage } from './list'; + +@NgModule({ + declarations: [ + AddonFilesListPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonFilesListPage), + TranslateModule.forChild() + ], +}) +export class AddonFilesListPageModule {} diff --git a/src/addon/files/pages/list/list.ts b/src/addon/files/pages/list/list.ts new file mode 100644 index 000000000..f79b3c19f --- /dev/null +++ b/src/addon/files/pages/list/list.ts @@ -0,0 +1,215 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, ViewChild, OnDestroy } from '@angular/core'; +import { IonicPage, NavParams, NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '../../../../providers/app'; +import { CoreEventsProvider } from '../../../../providers/events'; +import { CoreSitesProvider } from '../../../../providers/sites'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreTextUtilsProvider } from '../../../../providers/utils/text'; +import { AddonFilesProvider } from '../../providers/files'; +import { AddonFilesHelperProvider } from '../../providers/helper'; + +/** + * Page that displays the list of files. + */ +@IonicPage({ segment: 'addon-files-list' }) +@Component({ + selector: 'page-addon-files-list', + templateUrl: 'list.html', +}) +export class AddonFilesListPage implements OnDestroy { + + title: string; // Page title. + 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. + root: string; // The root of the files loaded: 'my' or 'site'. + path: string; // The path of the directory being loaded. If empty path it means the root is being loaded. + userQuota: number; // The user quota (in bytes). + filesInfo: any; // 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: any[]; // List of files. + component: string; // Component to link the file downloads to. + filesLoaded: boolean; // Whether the files are loaded. + + protected updateSiteObserver; + + constructor(navParams: NavParams, eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private appProvider: CoreAppProvider, + private filesProvider: AddonFilesProvider, private filesHelper: AddonFilesHelperProvider, + private textUtils: CoreTextUtilsProvider) { + this.title = navParams.get('title') || this.translate.instant('addon.files.files'); + this.root = navParams.get('root'); + this.path = navParams.get('path'); + + // Update visibility if current site info is updated. + this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, () => { + this.setVisibility(); + }, sitesProvider.getCurrentSiteId()); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.setVisibility(); + this.userQuota = this.sitesProvider.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; + } + } + + /** + * Refresh the data. + * + * @param {any} refresher Refresher. + */ + refreshData(refresher: any): void { + this.refreshFiles().finally(() => { + refresher.complete(); + }); + } + + /** + * Function called when the root has changed. + */ + rootChanged(): void { + this.filesLoaded = false; + this.component = this.root == 'my' ? AddonFilesProvider.PRIVATE_FILES_COMPONENT : AddonFilesProvider.SITE_FILES_COMPONENT; + + this.fetchFiles().finally(() => { + this.filesLoaded = true; + }); + } + + /** + * Upload a new file. + */ + uploadFile(): void { + this.filesProvider.versionCanUploadFiles().then((canUpload) => { + if (!canUpload) { + this.domUtils.showAlertTranslated('core.notice', 'addon.files.erroruploadnotworking'); + } else if (!this.appProvider.isOnline()) { + this.domUtils.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true); + } else { + this.filesHelper.uploadPrivateFile(this.filesInfo).then(() => { + // File uploaded, refresh the list. + this.filesLoaded = false; + this.refreshFiles().finally(() => { + this.filesLoaded = true; + }); + }).catch(() => { + // Ignore errors, they're handled inside the function. + }); + } + }); + } + + /** + * Set visibility of some items based on site data. + */ + protected setVisibility(): void { + this.showPrivateFiles = this.filesProvider.canViewPrivateFiles(); + this.showSiteFiles = this.filesProvider.canViewSiteFiles(); + this.showUpload = this.filesProvider.canUploadFiles(); + } + + /** + * Fetch the files. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchFiles(): Promise { + let promise; + + if (!this.path) { + // The path is unknown, the user must be requesting a root. + if (this.root == 'site') { + this.title = this.translate.instant('addon.files.sitefiles'); + promise = this.filesProvider.getSiteFiles(); + } else if (this.root == 'my') { + this.title = this.translate.instant('addon.files.files'); + + promise = this.filesProvider.getPrivateFiles().then((files) => { + if (this.showUpload && this.filesProvider.canGetPrivateFilesInfo() && this.userQuota > 0) { + // Get the info to calculate the available size. + return this.filesProvider.getPrivateFilesInfo().then((info) => { + this.filesInfo = info; + this.spaceUsed = this.textUtils.bytesToSize(info.filesizewithoutreferences, 1); + this.userQuotaReadable = this.textUtils.bytesToSize(this.userQuota, 1); + + return files; + }); + } else { + // User quota isn't useful, delete it. + delete this.userQuota; + } + + return files; + }); + } else { + // Unknown root. + promise = Promise.reject(null); + } + } else { + // Path is set, serve the files the user requested. + promise = this.filesProvider.getFiles(this.path); + } + + return promise.then((files) => { + this.files = files; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.files.couldnotloadfiles', true); + }); + } + + /** + * Refresh the displayed files. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshFiles(): Promise { + const promises = []; + + promises.push(this.filesProvider.invalidateDirectory(this.root, this.path)); + promises.push(this.filesProvider.invalidatePrivateFilesInfoForUser()); + + return Promise.all(promises).finally(() => { + return this.fetchFiles(); + }); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.updateSiteObserver && this.updateSiteObserver.off(); + } +} diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 87b800364..e1bd4a08c 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -204,7 +204,7 @@ export class CoreUtilsProvider { } return newArray; - } else if (typeof source == 'object') { + } else if (typeof source == 'object' && source !== null) { // Clone the object and all the subproperties. const newObject = {}; for (const name in source) { From 0d4542f00438ff26e6269d3caab4df0b1d356afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 24 Jan 2018 15:39:05 +0100 Subject: [PATCH 3/3] MOBILE-2322 files: Style changes --- src/addon/files/providers/helper.ts | 2 +- src/app/app.ios.scss | 7 ++++++ src/app/app.md.scss | 7 ++++++ src/app/app.scss | 25 ++++++++++++++++++- src/app/app.wp.scss | 7 ++++++ src/components/file/file.html | 4 +-- .../pages/capture-media/capture-media.html | 2 +- src/core/fileuploader/providers/helper.ts | 2 +- src/providers/utils/dom.ts | 4 ++- src/theme/variables.scss | 9 ++++++- 10 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/addon/files/providers/helper.ts b/src/addon/files/providers/helper.ts index 467cb0ad9..019cde0a0 100644 --- a/src/addon/files/providers/helper.ts +++ b/src/addon/files/providers/helper.ts @@ -68,7 +68,7 @@ export class AddonFilesHelperProvider { modal.dismiss(); }); }).then(() => { - this.domUtils.showAlertTranslated('core.success', 'core.fileuploader.fileuploaded'); + this.domUtils.showToast('core.fileuploader.fileuploaded', true, undefined, 'core-toast-success'); }); } } diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index ad9be8fdd..408be06f3 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -21,6 +21,13 @@ @include margin($item-ios-padding-icon-top, null, $item-ios-padding-icon-bottom, 0); } +@each $color-name, $color-base, $color-contrast in get-colors($colors-ios) { + .ios .core-#{$color-name}-card { + @extend .card-ios ; + @extend .card-content-ios; + } +} + // Highlights inside the input element. @if ($core-text-input-ios-show-highlight) { .card-ios, .list-ios { diff --git a/src/app/app.md.scss b/src/app/app.md.scss index b79bb1bdf..2f599babb 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -21,6 +21,13 @@ @include margin-horizontal($item-md-padding-start + ($item-md-padding-start / 2) - 1, null); } +@each $color-name, $color-base, $color-contrast in get-colors($colors-md) { + .md .core-#{$color-name}-card { + @extend .card-md; + @extend .card-content-md; + } +} + // Highlights inside the input element. @if ($core-text-input-md-show-highlight) { .card-md, .list-md { diff --git a/src/app/app.scss b/src/app/app.scss index 79b3808c7..540d8a4bf 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -408,4 +408,27 @@ ion-select { margin: 0 0 0 0.5em; max-width: calc(100% - 1em); } -/*rtl:end:ignore*/ \ No newline at end of file +/*rtl:end:ignore*/ + +.action-sheet-group { + overflow: auto; +} +.alert-message { + overflow-y: auto; +} + +ion-toast.core-toast-success .toast-wrapper{ + background: $green-dark; +} + +ion-toast.core-toast-alert .toast-wrapper{ + background: $red-dark; +} + +// Message cards +@each $color-name, $color-base, $color-contrast in get-colors($colors) { + .core-#{$color-name}-card { + @extend ion-card; + border-bottom: 3px solid $color-base; + } +} \ No newline at end of file diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index 312358980..8233ee3e4 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -20,3 +20,10 @@ .item-wp ion-spinner[item-start] + .item-input { @include margin-horizontal(($item-wp-padding-start / 2), null); } + +@each $color-name, $color-base, $color-contrast in get-colors($colors-wp) { + .wp .core-#{$color-name}-card { + @extend .card-wp ; + @extend .card-content-wp; + } +} diff --git a/src/components/file/file.html b/src/components/file/file.html index 9cf0d41c6..509ae3aca 100644 --- a/src/components/file/file.html +++ b/src/components/file/file.html @@ -1,8 +1,8 @@ - +

{{fileName}}

- - {{ title }} + {{ title | translate }} diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts index 7c56f9937..e955869a3 100644 --- a/src/core/fileuploader/providers/helper.ts +++ b/src/core/fileuploader/providers/helper.ts @@ -368,7 +368,7 @@ export class CoreFileUploaderHelperProvider { return this.fileProvider.getFileObjectFromFileEntry(fileEntry).then((file) => { return this.confirmUploadFile(file.size).then(() => { return this.uploadGenericFile(fileEntry.toURL(), file.name, file.type, deleteAfterUpload, siteId).then(() => { - this.domUtils.showAlertTranslated('core.success', 'core.fileuploader.fileuploaded'); + this.domUtils.showToast('core.fileuploader.fileuploaded', true, undefined, 'core-toast-success'); }); }).catch((err) => { if (err) { diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 3d9ea96fb..8d87fc766 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -848,9 +848,10 @@ export class CoreDomUtilsProvider { * @param {string} text The text of the toast. * @param {boolean} [needsTranslate] Whether the 'text' needs to be translated. * @param {number} [duration=2000] Duration in ms of the dimissable toast. + * @param {string} [cssClass=""] Class to add to the toast. * @return {Toast} Toast instance. */ - showToast(text: string, needsTranslate?: boolean, duration: number = 2000): Toast { + showToast(text: string, needsTranslate?: boolean, duration: number = 2000, cssClass: string = ''): Toast { if (needsTranslate) { text = this.translate.instant(text); } @@ -859,6 +860,7 @@ export class CoreDomUtilsProvider { message: text, duration: duration, position: 'bottom', + cssClass: cssClass, dismissOnPageChange: true }); diff --git a/src/theme/variables.scss b/src/theme/variables.scss index ffaec06dd..596cd09b9 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -78,7 +78,9 @@ $colors: ( light: $gray-lighter, gray: $gray-dark, dark: $black, - warning: $yellow + warning: $yellow, + success: $green, + info: $blue ); $text-color: $black; @@ -86,6 +88,11 @@ $link-color: $blue; $background-color: $gray-light; $subdued-text-color: $gray-darker; +$core-warning-color: colors($colors, warning) !default; // yellow. +$core-success-color: colors($colors, success) !default; // green. +$core-info-color: colors($colors, info) !default; // / blue. +$core-error-color: colors($colors, alert) !default; // Red. + $list-background-color: $white; $tabs-background: $gray-darker;