From 92f574235197ccfbf6cc092f82478f265ed653cb Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 10 Mar 2021 15:58:10 +0100 Subject: [PATCH 1/2] MOBILE-3630 sharefiles: Implement services --- src/app/app.component.ts | 2 +- src/core/features/compile/services/compile.ts | 4 +- src/core/features/features.module.ts | 2 + src/core/features/sharedfiles/lang.json | 11 + .../services/database/sharedfiles.ts | 43 +++ .../sharedfiles/services/handlers/settings.ts | 56 ++++ .../sharedfiles/services/handlers/upload.ts | 74 +++++ .../services/sharedfiles-helper.ts | 277 ++++++++++++++++++ .../sharedfiles/services/sharedfiles.ts | 269 +++++++++++++++++ .../sharedfiles/sharedfiles.module.ts | 64 ++++ src/core/singletons/events.ts | 17 ++ 11 files changed, 816 insertions(+), 3 deletions(-) create mode 100644 src/core/features/sharedfiles/lang.json create mode 100644 src/core/features/sharedfiles/services/database/sharedfiles.ts create mode 100644 src/core/features/sharedfiles/services/handlers/settings.ts create mode 100644 src/core/features/sharedfiles/services/handlers/upload.ts create mode 100644 src/core/features/sharedfiles/services/sharedfiles-helper.ts create mode 100644 src/core/features/sharedfiles/services/sharedfiles.ts create mode 100644 src/core/features/sharedfiles/sharedfiles.module.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c88c155cc..d81f14598 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -155,7 +155,7 @@ export class AppComponent implements OnInit, AfterViewInit { this.lastUrls[url] = Date.now(); - CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, url); + CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, { url }); CoreCustomURLSchemes.handleCustomURL(url).catch((error) => { CoreCustomURLSchemes.treatHandleCustomURLError(error); }); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 44b1ac0c2..c4b0aed05 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -57,7 +57,7 @@ import { CORE_LOGIN_SERVICES } from '@features/login/login.module'; import { CORE_MAINMENU_SERVICES } from '@features/mainmenu/mainmenu.module'; import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module'; import { CORE_QUESTION_SERVICES } from '@features/question/question.module'; -// @todo import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module'; +import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module'; import { CORE_RATING_SERVICES } from '@features/rating/rating.module'; import { CORE_SEARCH_SERVICES } from '@features/search/search.module'; import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module'; @@ -271,7 +271,7 @@ export class CoreCompileProvider { ...CORE_RATING_SERVICES, ...CORE_SEARCH_SERVICES, ...CORE_SETTINGS_SERVICES, - // @todo ...CORE_SHAREDFILES_SERVICES, + ...CORE_SHAREDFILES_SERVICES, ...CORE_SITEHOME_SERVICES, CoreSitePluginsProvider, ...CORE_TAG_SERVICES, diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index 9be217cb1..adbc3c687 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -33,6 +33,7 @@ import { CoreSearchModule } from './search/search.module'; import { CoreCommentsModule } from './comments/comments.module'; import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; import { CoreRatingModule } from './rating/rating.module'; +import { CoreSharedFilesModule } from './sharedfiles/sharedfiles.module'; @NgModule({ imports: [ @@ -55,6 +56,7 @@ import { CoreRatingModule } from './rating/rating.module'; CoreCommentsModule, CoreSitePluginsModule, CoreRatingModule, + CoreSharedFilesModule, ], }) export class CoreFeaturesModule {} diff --git a/src/core/features/sharedfiles/lang.json b/src/core/features/sharedfiles/lang.json new file mode 100644 index 000000000..c7ba93a40 --- /dev/null +++ b/src/core/features/sharedfiles/lang.json @@ -0,0 +1,11 @@ +{ + "chooseaccountstorefile": "Choose an account to store the file in.", + "chooseactionrepeatedfile": "A file with this name already exists. Do you want to replace the existing file or rename it to \"{{$a}}\"?", + "errorreceivefilenosites": "There are no sites stored. Please add a site before sharing a file with the app.", + "nosharedfiles": "There are no shared files stored on this site.", + "nosharedfilestoupload": "You have no files to upload here. If you want to upload a file from another app, locate the file and click the 'Open in' button.", + "rename": "Rename", + "replace": "Replace", + "sharedfiles": "Shared files", + "successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity." +} \ No newline at end of file diff --git a/src/core/features/sharedfiles/services/database/sharedfiles.ts b/src/core/features/sharedfiles/services/database/sharedfiles.ts new file mode 100644 index 000000000..346fae39f --- /dev/null +++ b/src/core/features/sharedfiles/services/database/sharedfiles.ts @@ -0,0 +1,43 @@ +// (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 { CoreAppSchema } from '@services/app'; + +/** + * Database variables for CoreSharedFilesProvider service. + */ +export const SHARED_FILES_TABLE_NAME = 'shared_files'; +export const APP_SCHEMA: CoreAppSchema = { + name: 'CoreSharedFilesProvider', + version: 1, + tables: [ + { + name: SHARED_FILES_TABLE_NAME, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true, + }, + ], + }, + ], +}; + +/** + * Data stored in DB for shared files. + */ +export type CoreSharedFilesDBRecord = { + id: string; +}; diff --git a/src/core/features/sharedfiles/services/handlers/settings.ts b/src/core/features/sharedfiles/services/handlers/settings.ts new file mode 100644 index 000000000..66cc467de --- /dev/null +++ b/src/core/features/sharedfiles/services/handlers/settings.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 { Injectable } from '@angular/core'; +import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; +import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; +import { CoreApp } from '@services/app'; +import { makeSingleton } from '@singletons'; + +/** + * Shared files settings handler. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesSettingsHandlerService implements CoreSettingsHandler { + + name = 'CoreSharedFiles'; + priority = 200; + + /** + * 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 CoreApp.isIOS(); + } + + /** + * Returns the data needed to render the handler. + * + * @return Data needed to render the handler. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + icon: 'fas-folder', + title: 'core.sharedfiles.sharedfiles', + page: SHAREDFILES_PAGE_NAME + '/list/root', + params: { manage: true, hideSitePicker: true }, + class: 'core-sharedfiles-settings-handler', + }; + } + +} + +export const CoreSharedFilesSettingsHandler = makeSingleton(CoreSharedFilesSettingsHandlerService); diff --git a/src/core/features/sharedfiles/services/handlers/upload.ts b/src/core/features/sharedfiles/services/handlers/upload.ts new file mode 100644 index 000000000..9ee934de3 --- /dev/null +++ b/src/core/features/sharedfiles/services/handlers/upload.ts @@ -0,0 +1,74 @@ +// (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 { + CoreFileUploaderHandler, + CoreFileUploaderHandlerData, + CoreFileUploaderHandlerResult, +} from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreApp } from '@services/app'; +import { makeSingleton } from '@singletons'; +import { CoreSharedFilesHelper } from '../sharedfiles-helper'; +/** + * Handler to upload files from the album. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesUploadHandlerService implements CoreFileUploaderHandler { + + name = 'CoreSharedFilesUpload'; + priority = 1300; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return CoreApp.isIOS(); + } + + /** + * Given a list of mimetypes, return the ones that are supported by the handler. + * + * @param mimetypes List of mimetypes. + * @return Supported mimetypes. + */ + getSupportedMimetypes(mimetypes: string[]): string[] { + return mimetypes; + } + + /** + * Get the data to display the handler. + * + * @return Data. + */ + getData(): CoreFileUploaderHandlerData { + return { + title: 'core.sharedfiles.sharedfiles', + class: 'core-sharedfiles-fileuploader-handler', + icon: 'folder', + action: ( + maxSize?: number, + upload?: boolean, + allowOffline?: boolean, + mimetypes?: string[], + ): Promise => CoreSharedFilesHelper.pickSharedFile(mimetypes), + }; + } + +} + +export const CoreSharedFilesUploadHandler = makeSingleton(CoreSharedFilesUploadHandlerService); diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts new file mode 100644 index 000000000..704c0fb9a --- /dev/null +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -0,0 +1,277 @@ +// (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 { FileEntry } from '@ionic-native/file'; + +import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreFileUploaderHandlerResult } from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { AlertController, ApplicationInit, makeSingleton, ModalController, Platform, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSharedFilesListModalComponent } from '../components/list-modal/list-modal'; +import { CoreSharedFiles } from './sharedfiles'; +import { SHAREDFILES_PAGE_NAME } from '../sharedfiles.module'; +import { CoreSharedFilesChooseSitePage } from '../pages/choose-site/choose-site'; +import { CoreError } from '@classes/errors/error'; + +/** + * Helper service to share files with the app. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesHelperProvider { + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSharedFilesHelperProvider'); + } + + /** + * Initialize. + */ + initialize(): void { + if (!CoreApp.isIOS()) { + return; + } + + let lastCheck = Date.now(); + + // Check if there are new files at app start and when the app is resumed. + this.searchIOSNewSharedFiles(); + + Platform.resume.subscribe(() => { + // Wait a bit to make sure that APP_LAUNCHED_URL is treated before this callback. + setTimeout(() => { + if (Date.now() - lastCheck < 1000) { + // Last check less than 1s ago, don't do anything. + return; + } + + lastCheck = Date.now(); + this.searchIOSNewSharedFiles(); + }, 200); + }); + + CoreEvents.on(CoreEvents.APP_LAUNCHED_URL, (data) => { + if (data.url.indexOf('file://') === 0) { + // We received a file in iOS, it's probably a shared file. Treat it. + lastCheck = Date.now(); + this.searchIOSNewSharedFiles(data.url); + } + }); + } + + /** + * Ask a user if he wants to replace a file (using originalName) or rename it (using newName). + * + * @param originalName Original name. + * @param newName New name. + * @return Promise resolved with the name to use when the user chooses. Rejected if user cancels. + */ + async askRenameReplace(originalName: string, newName: string): Promise { + const alert = await AlertController.create({ + header: Translate.instant('core.sharedfiles.sharedfiles'), + message: Translate.instant('core.sharedfiles.chooseactionrepeatedfile', { $a: newName }), + buttons: [ + { + text: Translate.instant('core.sharedfiles.rename'), + role: 'rename', + }, + { + text: Translate.instant('core.sharedfiles.replace'), + role: 'replace', + }, + ], + }); + + await alert.present(); + + const result = await alert.onDidDismiss(); + + if (result.role == 'rename') { + return newName; + } else if (result.role == 'replace') { + return originalName; + } else { + // Canceled. + throw new CoreCanceledError(); + } + } + + /** + * Go to the choose site view. + * + * @param filePath File path to send to the view. + * @param isInbox Whether the file is in the Inbox folder. + */ + goToChooseSite(filePath: string, isInbox?: boolean): void { + if (CoreSites.isLoggedIn()) { + CoreNavigator.navigateToSitePath(`/${SHAREDFILES_PAGE_NAME}/choosesite`, { + params: { filePath, isInbox }, + }); + } else { + CoreNavigator.navigate(`/${SHAREDFILES_PAGE_NAME}/choosesite`, { + params: { filePath, isInbox }, + }); + } + } + + /** + * Whether the user is already choosing a site to store a shared file. + * + * @return Whether the user is already choosing a site to store a shared file. + */ + protected isChoosingSite(): boolean { + return CoreNavigator.getCurrentRoute({ pageComponent: CoreSharedFilesChooseSitePage }) !== null; + } + + /** + * Open the view to select a shared file. + * + * @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported. + * @return Promise resolved when a file is picked, rejected if file picker is closed without selecting a file. + */ + async pickSharedFile(mimetypes?: string[]): Promise { + const modal = await ModalController.create({ + component: CoreSharedFilesListModalComponent, + cssClass: 'core-modal-fullscreen', + componentProps: { mimetypes, pick: true }, + }); + + await modal.present(); + + const result = await modal.onDidDismiss(); + const file: FileEntry | undefined = result.data; + + if (!file) { + // User cancelled. + throw new CoreCanceledError(); + } + + const error = CoreFileUploader.isInvalidMimetype(mimetypes, file.fullPath); + if (error) { + throw new CoreError(error); + } + + return { + path: file.fullPath, + treated: false, + }; + } + + /** + * Delete a shared file. + * + * @param fileEntry The file entry to delete. + * @param isInbox Whether the file is in the Inbox folder. + * @return Promise resolved when done. + */ + protected removeSharedFile(fileEntry: FileEntry, isInbox?: boolean): Promise { + if (isInbox) { + return CoreSharedFiles.deleteInboxFile(fileEntry); + } else { + return CoreFile.removeFileByFileEntry(fileEntry); + } + } + + /** + * Checks if there is a new file received in iOS and move it to the shared folder of current site. + * If more than one site is found, the user will have to choose the site where to store it in. + * If more than one file is found, treat only the first one. + * + * @param path Path to a file received when launching the app. + * @return Promise resolved when done. + */ + async searchIOSNewSharedFiles(path?: string): Promise { + try { + await ApplicationInit.donePromise; + + if (this.isChoosingSite()) { + // We're already treating a shared file. Abort. + return; + } + + let fileEntry: FileEntry | undefined; + if (path) { + // The app was launched with the path to the file, get the file. + fileEntry = await CoreFile.getExternalFile(path); + } else { + // No path received, search if there is any file in the Inbox folder. + fileEntry = await CoreSharedFiles.checkIOSNewFiles(); + } + + if (!fileEntry) { + return; + } + + const siteIds = await CoreSites.getSitesIds(); + + if (!siteIds.length) { + // No sites stored, show error and delete the file. + CoreDomUtils.showErrorModal('core.sharedfiles.errorreceivefilenosites', true); + + return this.removeSharedFile(fileEntry, !path); + } else if (siteIds.length == 1) { + return this.storeSharedFileInSite(fileEntry, siteIds[0], !path); + } else if (!this.isChoosingSite()) { + this.goToChooseSite(fileEntry.toURL(), !path); + } + } catch (error) { + if (error) { + this.logger.error('Error searching iOS new shared files', error, path); + } + } + } + + /** + * Store a shared file in a site's shared files folder. + * + * @param fileEntry Shared file entry. + * @param siteId Site ID. If not defined, current site. + * @param isInbox Whether the file is in the Inbox folder. + * @return Promise resolved when done. + */ + async storeSharedFileInSite(fileEntry: FileEntry, siteId?: string, isInbox?: boolean): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // First of all check if there's already a file with the same name in the shared files folder. + const sharedFilesDirPath = CoreSharedFiles.getSiteSharedFilesDirPath(siteId); + + let newName = await CoreFile.getUniqueNameInFolder(sharedFilesDirPath, fileEntry.name); + + if (newName.toLowerCase() != fileEntry.name.toLowerCase()) { + // Repeated name. Ask the user what he wants to do. + newName = await this.askRenameReplace(fileEntry.name, newName); + } + + try { + await CoreSharedFiles.storeFileInSite(fileEntry, newName, siteId); + } catch (error) { + CoreDomUtils.showErrorModal(error || 'Error moving file.'); + } finally { + this.removeSharedFile(fileEntry, isInbox); + CoreDomUtils.showAlertTranslated('core.success', 'core.sharedfiles.successstorefile'); + } + } + +} + +export const CoreSharedFilesHelper = makeSingleton(CoreSharedFilesHelperProvider); diff --git a/src/core/features/sharedfiles/services/sharedfiles.ts b/src/core/features/sharedfiles/services/sharedfiles.ts new file mode 100644 index 000000000..a774aea78 --- /dev/null +++ b/src/core/features/sharedfiles/services/sharedfiles.ts @@ -0,0 +1,269 @@ +// (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 { FileEntry, DirectoryEntry } from '@ionic-native/file'; +import { Md5 } from 'ts-md5/dist/md5'; + +import { SQLiteDB } from '@classes/sqlitedb'; +import { CoreLogger } from '@singletons/logger'; +import { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreSites } from '@services/sites'; +import { CoreEvents } from '@singletons/events'; +import { makeSingleton } from '@singletons'; +import { APP_SCHEMA, CoreSharedFilesDBRecord, SHARED_FILES_TABLE_NAME } from './database/sharedfiles'; + +/** + * Service to share files with the app. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSharedFilesProvider { + + static readonly SHARED_FILES_FOLDER = 'sharedfiles'; + + protected logger: CoreLogger; + // Variables for DB. + protected appDB: Promise; + protected resolveAppDB!: (appDB: SQLiteDB) => void; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSharedFilesProvider'); + this.appDB = new Promise(resolve => this.resolveAppDB = resolve); + } + + /** + * Initialize database. + * + * @return Promise resolved when done. + */ + async initializeDatabase(): Promise { + try { + await CoreApp.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + this.resolveAppDB(CoreApp.getDB()); + } + + /** + * Checks if there is a new file received in iOS. If more than one file is found, treat only the first one. + * The file returned is marked as "treated" and will be deleted in the next execution. + * + * @return Promise resolved with a new file to be treated. If no new files found, resolved with undefined. + */ + async checkIOSNewFiles(): Promise { + this.logger.debug('Search for new files on iOS'); + + const entries = await CoreUtils.ignoreErrors(CoreFile.getDirectoryContents('Inbox')); + + if (!entries || !entries.length) { + return; + } + + let fileToReturn: FileEntry | undefined; + + for (let i = 0; i < entries.length; i++) { + if (entries[i].isDirectory) { + continue; + } + + const fileEntry = entries[i]; + const fileId = this.getFileId(fileEntry); + + try { + // Check if file was already treated. + await this.isFileTreated(fileId); + + // File already treated, delete it. No need to block the execution for this. + this.deleteInboxFile(fileEntry); + } catch { + // File not treated before. + this.logger.debug(`Found new file ${fileEntry.name} shared with the app.`); + fileToReturn = fileEntry; + break; + } + } + + if (!fileToReturn) { + return; + } + + // Mark it as "treated". + const fileId = this.getFileId(fileToReturn); + + await this.markAsTreated(fileId); + + this.logger.debug(`File marked as "treated": ${fileToReturn.name}`); + + return fileToReturn; + } + + /** + * Deletes a file in the Inbox folder (shared with the app). + * + * @param entry FileEntry. + * @return Promise resolved when done, rejected otherwise. + */ + async deleteInboxFile(entry: FileEntry): Promise { + this.logger.debug('Delete inbox file: ' + entry.name); + + await CoreUtils.ignoreErrors(CoreFile.removeFileByFileEntry(entry)); + + try { + await this.unmarkAsTreated(this.getFileId(entry)); + + this.logger.debug(`"Treated" mark removed from file: ${entry.name}`); + } catch (error) { + this.logger.debug(`Error deleting "treated" mark from file: ${entry.name}`, error); + + throw error; + } + } + + /** + * Get the ID of a file for managing "treated" files. + * + * @param entry FileEntry. + * @return File ID. + */ + protected getFileId(entry: FileEntry): string { + return Md5.hashAsciiStr(entry.name); + } + + /** + * Get the shared files stored in a site. + * + * @param siteId Site ID. If not defined, current site. + * @param path Path to search inside the site shared folder. + * @param mimetypes List of supported mimetypes. If undefined, all mimetypes supported. + * @return Promise resolved with the files. + */ + async getSiteSharedFiles(siteId?: string, path?: string, mimetypes?: string[]): Promise<(FileEntry | DirectoryEntry)[]> { + let pathToGet = this.getSiteSharedFilesDirPath(siteId); + if (path) { + pathToGet = CoreTextUtils.concatenatePaths(pathToGet, path); + } + + try { + let entries = await CoreFile.getDirectoryContents(pathToGet); + + if (mimetypes) { + // Get only files with the right mimetype and the ones we cannot determine the mimetype. + entries = entries.filter((entry) => { + const extension = CoreMimetypeUtils.getFileExtension(entry.name); + const mimetype = CoreMimetypeUtils.getMimeType(extension); + + return !mimetype || mimetypes.indexOf(mimetype) > -1; + }); + } + + return entries; + } catch { + // Directory not found, return empty list. + return []; + } + } + + /** + * Get the path to a site's shared files folder. + * + * @param siteId Site ID. If not defined, current site. + * @return Path. + */ + getSiteSharedFilesDirPath(siteId?: string): string { + siteId = siteId || CoreSites.getCurrentSiteId(); + + return CoreFile.getSiteFolder(siteId) + '/' + CoreSharedFilesProvider.SHARED_FILES_FOLDER; + } + + /** + * Check if a file has been treated already. + * + * @param fileId File ID. + * @return Resolved if treated, rejected otherwise. + */ + protected async isFileTreated(fileId: string): Promise { + const db = await this.appDB; + + return db.getRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + + /** + * Mark a file as treated. + * + * @param fileId File ID. + * @return Promise resolved when marked. + */ + protected async markAsTreated(fileId: string): Promise { + try { + // Check if it's already marked. + await this.isFileTreated(fileId); + } catch (err) { + // Doesn't exist, insert it. + const db = await this.appDB; + + await db.insertRecord(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + } + + /** + * Store a file in a site's shared folder. + * + * @param entry File entry. + * @param newName Name of the new file. If not defined, use original file's name. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async storeFileInSite(entry: FileEntry, newName?: string, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + if (!entry || !siteId) { + return; + } + + newName = newName || entry.name; + + const sharedFilesFolder = this.getSiteSharedFilesDirPath(siteId); + const newPath = CoreTextUtils.concatenatePaths(sharedFilesFolder, newName); + + // Create dir if it doesn't exist already. + await CoreFile.createDir(sharedFilesFolder); + + const newFile = await CoreFile.moveExternalFile(entry.toURL(), newPath); + + CoreEvents.trigger(CoreEvents.FILE_SHARED, { siteId, name: newName }); + + return newFile; + } + + /** + * Unmark a file as treated. + * + * @param fileId File ID. + * @return Resolved when unmarked. + */ + protected async unmarkAsTreated(fileId: string): Promise { + const db = await this.appDB; + + await db.deleteRecords(SHARED_FILES_TABLE_NAME, { id: fileId }); + } + +} + +export const CoreSharedFiles = makeSingleton(CoreSharedFilesProvider); diff --git a/src/core/features/sharedfiles/sharedfiles.module.ts b/src/core/features/sharedfiles/sharedfiles.module.ts new file mode 100644 index 000000000..5d82b264a --- /dev/null +++ b/src/core/features/sharedfiles/sharedfiles.module.ts @@ -0,0 +1,64 @@ +// (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 { AppRoutingModule } from '@/app/app-routing.module'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; + +import { CoreFileUploaderDelegate } from '@features/fileuploader/services/fileuploader-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; +import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; +import { CoreSharedFilesComponentsModule } from './components/components.module'; +import { CoreSharedFilesSettingsHandler } from './services/handlers/settings'; +import { CoreSharedFilesUploadHandler } from './services/handlers/upload'; +import { CoreSharedFiles, CoreSharedFilesProvider } from './services/sharedfiles'; +import { CoreSharedFilesHelper, CoreSharedFilesHelperProvider } from './services/sharedfiles-helper'; + +export const CORE_SHAREDFILES_SERVICES: Type[] = [ + CoreSharedFilesProvider, + CoreSharedFilesHelperProvider, +]; + +export const SHAREDFILES_PAGE_NAME = 'sharedfiles'; + +const routes: Routes = [ + { + path: SHAREDFILES_PAGE_NAME, + loadChildren: () => import('./sharedfiles-lazy.module').then(m => m.CoreSharedFilesLazyModule), + }, +]; + +@NgModule({ + imports: [ + AppRoutingModule.forChild(routes), + CoreMainMenuTabRoutingModule.forChild(routes), + CoreSitePreferencesRoutingModule.forChild(routes), + CoreSharedFilesComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useFactory: () => async () => { + CoreFileUploaderDelegate.registerHandler(CoreSharedFilesUploadHandler.instance); + CoreSettingsDelegate.registerHandler(CoreSharedFilesSettingsHandler.instance); + + CoreSharedFilesHelper.initialize(); + await CoreSharedFiles.initializeDatabase(); + }, + }, + ], +}) +export class CoreSharedFilesModule {} diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 14a44e3ff..8b433b74e 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -51,6 +51,8 @@ export interface CoreEventsData { [CoreEvents.LOGIN_SITE_CHECKED]: CoreEventLoginSiteCheckedData; [CoreEvents.SEND_ON_ENTER_CHANGED]: CoreEventSendOnEnterChangedData; [CoreEvents.COMPONENT_FILE_ACTION]: CoreFilepoolComponentFileEventData; + [CoreEvents.FILE_SHARED]: CoreEventFileSharedData; + [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData; }; /* @@ -359,3 +361,18 @@ export type CoreEventLoginSiteCheckedData = { export type CoreEventSendOnEnterChangedData = { sendOnEnter: boolean; }; + +/** + * Data passed to FILE_SHARED event. + */ +export type CoreEventFileSharedData = { + name: string; + siteId: string; +}; + +/** + * Data passed to APP_LAUNCHED_URL event. + */ + export type CoreEventAppLaunchedData = { + url: string; +}; From 2302638702c2b4f434524dc70999c5d9c146c7d6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 11 Mar 2021 14:33:46 +0100 Subject: [PATCH 2/2] MOBILE-3630 sharedfiled: Implement components and pages --- src/core/classes/page-items-list-manager.ts | 2 +- src/core/components/components.module.ts | 3 + .../site-picker/core-site-picker.html | 6 + .../components/site-picker/site-picker.ts | 77 ++++++++ .../services/fileuploader-helper.ts | 24 +-- src/core/features/settings/constants.ts | 55 ------ .../features/settings/pages/index/index.html | 11 +- .../features/settings/pages/index/index.ts | 71 +++++++- .../features/settings/pages/site/site.html | 12 +- src/core/features/settings/pages/site/site.ts | 26 +-- .../settings/services/settings-helper.ts | 26 +++ .../features/settings/settings-lazy.module.ts | 6 +- .../components/components.module.ts | 34 ++++ .../components/list-modal/list-modal.html | 19 ++ .../components/list-modal/list-modal.ts | 75 ++++++++ .../sharedfiles/components/list/list.html | 32 ++++ .../sharedfiles/components/list/list.ts | 170 ++++++++++++++++++ .../pages/choose-site/choose-site.html | 32 ++++ .../pages/choose-site/choose-site.ts | 108 +++++++++++ .../features/sharedfiles/pages/list/list.html | 13 ++ .../features/sharedfiles/pages/list/list.ts | 63 +++++++ .../sharedfiles/sharedfiles-lazy.module.ts | 45 +++++ src/core/services/navigator.ts | 15 ++ src/core/singletons/events.ts | 2 +- 24 files changed, 811 insertions(+), 116 deletions(-) create mode 100644 src/core/components/site-picker/core-site-picker.html create mode 100644 src/core/components/site-picker/site-picker.ts delete mode 100644 src/core/features/settings/constants.ts create mode 100644 src/core/features/sharedfiles/components/components.module.ts create mode 100644 src/core/features/sharedfiles/components/list-modal/list-modal.html create mode 100644 src/core/features/sharedfiles/components/list-modal/list-modal.ts create mode 100644 src/core/features/sharedfiles/components/list/list.html create mode 100644 src/core/features/sharedfiles/components/list/list.ts create mode 100644 src/core/features/sharedfiles/pages/choose-site/choose-site.html create mode 100644 src/core/features/sharedfiles/pages/choose-site/choose-site.ts create mode 100644 src/core/features/sharedfiles/pages/list/list.html create mode 100644 src/core/features/sharedfiles/pages/list/list.ts create mode 100644 src/core/features/sharedfiles/sharedfiles-lazy.module.ts diff --git a/src/core/classes/page-items-list-manager.ts b/src/core/classes/page-items-list-manager.ts index defd9ca3d..ed43becdd 100644 --- a/src/core/classes/page-items-list-manager.ts +++ b/src/core/classes/page-items-list-manager.ts @@ -137,7 +137,7 @@ export abstract class CorePageItemsListManager { // If this item is already selected, do nothing. const itemRoute = this.getItemRoute(route); const itemPath = this.getItemPath(item); - const selectedItemPath = itemRoute ? this.getSelectedItemPath(itemRoute.snapshot) : null; + const selectedItemPath = itemRoute?.snapshot ? this.getSelectedItemPath(itemRoute.snapshot) : null; if (selectedItemPath === itemPath) { return; diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index 4e37a8a76..8e1f2bfe6 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -52,6 +52,7 @@ import { CoreAttachmentsComponent } from './attachments/attachments'; import { CoreFilesComponent } from './files/files'; import { CoreLocalFileComponent } from './local-file/local-file'; import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip'; +import { CoreSitePickerComponent } from './site-picker/site-picker'; @NgModule({ declarations: [ @@ -86,6 +87,7 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip'; CoreFilesComponent, CoreLocalFileComponent, CoreBSTooltipComponent, + CoreSitePickerComponent, ], imports: [ CommonModule, @@ -127,6 +129,7 @@ import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip'; CoreFilesComponent, CoreLocalFileComponent, CoreBSTooltipComponent, + CoreSitePickerComponent, ], }) export class CoreComponentsModule {} diff --git a/src/core/components/site-picker/core-site-picker.html b/src/core/components/site-picker/core-site-picker.html new file mode 100644 index 000000000..ff07a6a1e --- /dev/null +++ b/src/core/components/site-picker/core-site-picker.html @@ -0,0 +1,6 @@ + + {{ 'core.site' | translate }} + + {{ site.fullNameAndSiteName }} + + diff --git a/src/core/components/site-picker/site-picker.ts b/src/core/components/site-picker/site-picker.ts new file mode 100644 index 000000000..8cdec8a16 --- /dev/null +++ b/src/core/components/site-picker/site-picker.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 { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; + +import { CoreFilter } from '@features/filter/services/filter'; +import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; + +/** + * Component to display a site selector. It will display a select with the list of sites. If the selected site changes, + * an output will be emitted with the site ID. + * + * Example usage: + * + */ +@Component({ + selector: 'core-site-picker', + templateUrl: 'core-site-picker.html', +}) +export class CoreSitePickerComponent implements OnInit { + + @Input() initialSite?: string; // Initial site. If not provided, current site. + @Output() siteSelected = new EventEmitter(); // Emit an event when a site is selected. Sends the siteId as parameter. + + selectedSite?: string; + sites?: SiteInfo[]; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.selectedSite = this.initialSite || CoreSites.getCurrentSiteId(); + + // Load the sites. + const sites = await CoreSites.getSites(); + + if (!this.selectedSite && sites.length) { + // There is no current site, select the first one. + this.selectedSite = sites[0].id; + this.siteSelected.emit(this.selectedSite); + } + + await Promise.all(sites.map(async (site: SiteInfo) => { + // Format the site name. + const options = { clean: true, singleLine: true, filter: false }; + const siteName = await CoreUtils.ignoreErrors( + CoreFilter.formatText(site.siteName || '', options, [], site.id), + site.siteName || '', + ); + + site.fullNameAndSiteName = Translate.instant( + 'core.fullnameandsitename', + { fullname: site.fullName, sitename: siteName }, + ); + })); + + this.sites = sites; + } + +} + +type SiteInfo = CoreSiteBasicInfo & { + fullNameAndSiteName?: string; +}; diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index a42747761..e0e939f57 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -376,16 +376,14 @@ export class CoreFileUploaderHelperProvider { try { const data = await handler.action(maxSize, upload, allowOffline, handler.mimetypes); + let result: CoreWSUploadFileResult | FileEntry | undefined; + if (data.treated) { // The handler already treated the file. Return the result. - this.fileUploaded(data.result!); - - return true; + result = data.result; } else if (data.fileEntry) { // The handler provided us a fileEntry, use it. - await this.uploadFileEntry(data.fileEntry, !!data.delete, maxSize, upload, allowOffline); - - return true; + result = await this.uploadFileEntry(data.fileEntry, !!data.delete, maxSize, upload, allowOffline); } else if (data.path) { let fileEntry: FileEntry; @@ -398,13 +396,17 @@ export class CoreFileUploaderHelperProvider { } // File found, treat it. - await this.uploadFileEntry(fileEntry, !!data.delete, maxSize, upload, allowOffline); - - return true; + result = await this.uploadFileEntry(fileEntry, !!data.delete, maxSize, upload, allowOffline); } - // Nothing received, fail. - throw new CoreError('No file received'); + if (!result) { + // Nothing received, fail. + throw new CoreError('No file received'); + } + + this.fileUploaded(result); + + return true; } catch (error) { CoreDomUtils.showErrorModalDefault( error, diff --git a/src/core/features/settings/constants.ts b/src/core/features/settings/constants.ts deleted file mode 100644 index 67b3eda4e..000000000 --- a/src/core/features/settings/constants.ts +++ /dev/null @@ -1,55 +0,0 @@ -// (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'; - -/** - * Settings section. - */ -export type CoreSettingsSection = { - name: string; - path: string; - icon: string; -}; - -/** - * Settings constants. - */ -export class CoreSettingsConstants { - - static readonly SECTIONS: CoreSettingsSection[] = [ - { - name: 'general', - path: 'general', - icon: 'fas-wrench', - }, - { - name: 'spaceusage', - path: 'spaceusage', - icon: 'fas-tasks', - }, - { - name: 'synchronization', - path: 'sync', - icon: CoreConstants.ICON_SYNC, - }, - // @TODO sharedfiles - { - name: 'about', - path: 'about', - icon: 'fas-id-card', - }, - ]; - -} diff --git a/src/core/features/settings/pages/index/index.html b/src/core/features/settings/pages/index/index.html index 2552bd392..5acca83d3 100644 --- a/src/core/features/settings/pages/index/index.html +++ b/src/core/features/settings/pages/index/index.html @@ -10,15 +10,10 @@ - + - {{ 'core.settings.' + section.name | translate }} + {{ section.name | translate }} diff --git a/src/core/features/settings/pages/index/index.ts b/src/core/features/settings/pages/index/index.ts index 00e452db5..14abe5b7a 100644 --- a/src/core/features/settings/pages/index/index.ts +++ b/src/core/features/settings/pages/index/index.ts @@ -13,10 +13,14 @@ // limitations under the License. import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; -import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants'; +import { ActivatedRouteSnapshot, Params } from '@angular/router'; + import { CorePageItemsListManager } from '@classes/page-items-list-manager'; -import { ActivatedRouteSnapshot } from '@angular/router'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreSettingsHelper } from '@features/settings/services/settings-helper'; +import { CoreConstants } from '@/core/constants'; +import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module'; +import { CoreApp } from '@services/app'; @Component({ selector: 'page-core-settings-index', @@ -32,7 +36,7 @@ export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy { * @inheritdoc */ ngAfterViewInit(): void { - this.sections.setItems(CoreSettingsConstants.SECTIONS); + this.sections.setItems(this.getSections()); this.sections.start(this.splitView); } @@ -43,6 +47,48 @@ export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy { this.sections.destroy(); } + /** + * Get the sections. + * + * @returns Sections. + */ + protected getSections(): CoreSettingsSection[] { + const sections: CoreSettingsSection[] = [ + { + name: 'core.settings.general', + path: 'general', + icon: 'fas-wrench', + }, + { + name: 'core.settings.spaceusage', + path: 'spaceusage', + icon: 'fas-tasks', + }, + { + name: 'core.settings.synchronization', + path: 'sync', + icon: CoreConstants.ICON_SYNC, + }, + ]; + + if (CoreApp.isIOS()) { + sections.push({ + name: 'core.sharedfiles.sharedfiles', + path: SHAREDFILES_PAGE_NAME + '/list/root', + icon: 'fas-folder', + params: { manage: true }, + }); + } + + sections.push({ + name: 'core.settings.about', + path: 'about', + icon: 'fas-id-card', + }); + + return sections; + } + } /** @@ -57,11 +103,28 @@ class CoreSettingsSectionsManager extends CorePageItemsListManager - diff --git a/src/core/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index 6fabd9e42..ec5ccc7c6 100644 --- a/src/core/features/settings/pages/site/site.ts +++ b/src/core/features/settings/pages/site/site.ts @@ -20,7 +20,6 @@ import { CoreSettingsDelegate, CoreSettingsHandlerToDisplay } from '../../servic import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -// import { CoreSharedFiles } from '@features/sharedfiles/services/sharedfiles'; import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings-helper'; import { CoreApp } from '@services/app'; import { CoreSiteInfo } from '@classes/site'; @@ -52,7 +51,6 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { spaceUsage: 0, }; - iosSharedFiles = 0; protected sitesObserver: CoreEventObserver; protected isDestroyed = false; @@ -101,25 +99,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy { this.siteName = currentSite!.getSiteName(); this.siteUrl = currentSite!.getURL(); - const promises: Promise[] = []; - - promises.push(CoreSettingsHelper.getSiteSpaceUsage(this.siteId) - .then((spaceUsage) => { - this.spaceUsage = spaceUsage; - - return; - })); - - /* if (this.isIOS) { - promises.push(CoreSharedFiles.getSiteSharedFiles(this.siteId) - .then((files) => { - this.iosSharedFiles = files.length; - - return; - })); - }*/ - - await Promise.all(promises); + this.spaceUsage = await CoreSettingsHelper.getSiteSpaceUsage(this.siteId); } /** @@ -224,9 +204,7 @@ class CoreSettingsSitePreferencesManager extends CorePageItemsListManager m.CoreSettingsSynchronizationPageModule), }, - // @todo sharedfiles + { + path: SHAREDFILES_PAGE_NAME, + loadChildren: () => import('@features/sharedfiles/sharedfiles-lazy.module').then(m => m.CoreSharedFilesLazyModule), + }, { path: 'about', loadChildren: () => import('./pages/about/about.module').then(m => m.CoreSettingsAboutPageModule), diff --git a/src/core/features/sharedfiles/components/components.module.ts b/src/core/features/sharedfiles/components/components.module.ts new file mode 100644 index 000000000..32b1a9f30 --- /dev/null +++ b/src/core/features/sharedfiles/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 { CoreSharedFilesListComponent } from './list/list'; +import { CoreSharedFilesListModalComponent } from './list-modal/list-modal'; + +@NgModule({ + declarations: [ + CoreSharedFilesListComponent, + CoreSharedFilesListModalComponent, + ], + imports: [ + CoreSharedModule, + ], + exports: [ + CoreSharedFilesListComponent, + CoreSharedFilesListModalComponent, + ], +}) +export class CoreSharedFilesComponentsModule {} diff --git a/src/core/features/sharedfiles/components/list-modal/list-modal.html b/src/core/features/sharedfiles/components/list-modal/list-modal.html new file mode 100644 index 000000000..2d6998369 --- /dev/null +++ b/src/core/features/sharedfiles/components/list-modal/list-modal.html @@ -0,0 +1,19 @@ + + + + + + {{ title }} + + + + + + + + + + + + diff --git a/src/core/features/sharedfiles/components/list-modal/list-modal.ts b/src/core/features/sharedfiles/components/list-modal/list-modal.ts new file mode 100644 index 000000000..c06f27796 --- /dev/null +++ b/src/core/features/sharedfiles/components/list-modal/list-modal.ts @@ -0,0 +1,75 @@ +// (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, Input } from '@angular/core'; +import { FileEntry } from '@ionic-native/file'; + +import { CoreFile } from '@services/file'; +import { ModalController, Translate } from '@singletons'; + +/** + * Modal to display the list of shared files. + */ +@Component({ + selector: 'core-shared-files-list-modal', + templateUrl: 'list-modal.html', +}) +export class CoreSharedFilesListModalComponent implements OnInit { + + @Input() siteId?: string; + @Input() mimetypes?: string[]; + @Input() manage?: boolean; + @Input() pick?: boolean; // To pick a file you MUST use a modal. + @Input() path?: string; + @Input() showSitePicker?: boolean; + + title?: string; + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.calculateTitle(this.path); + } + + /** + * Calculate the title. + * + * @param path Path to use. + */ + calculateTitle(path?: string): void { + if (path) { + this.title = CoreFile.getFileAndDirectoryFromPath(path).name; + } else { + this.title = Translate.instant('core.sharedfiles.sharedfiles'); + } + } + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + + /** + * A file was picked. + * + * @param file Picked file. + */ + filePicked(file: FileEntry): void { + ModalController.dismiss(file); + } + +} diff --git a/src/core/features/sharedfiles/components/list/list.html b/src/core/features/sharedfiles/components/list/list.html new file mode 100644 index 000000000..558525bdb --- /dev/null +++ b/src/core/features/sharedfiles/components/list/list.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + {{ file.name }} + + + + + + + + + + diff --git a/src/core/features/sharedfiles/components/list/list.ts b/src/core/features/sharedfiles/components/list/list.ts new file mode 100644 index 000000000..d915ba50f --- /dev/null +++ b/src/core/features/sharedfiles/components/list/list.ts @@ -0,0 +1,170 @@ +// (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, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { FileEntry, DirectoryEntry } from '@ionic-native/file'; +import { IonRefresher } from '@ionic/angular'; +import { Md5 } from 'ts-md5'; + +import { CoreSharedFiles } from '@features/sharedfiles/services/sharedfiles'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; + +/** + * Component to display the list of shared files, either as a modal or inside a page. + */ +@Component({ + selector: 'core-shared-files-list', + templateUrl: 'list.html', +}) +export class CoreSharedFilesListComponent implements OnInit, OnDestroy { + + @Input() siteId?: string; + @Input() mimetypes?: string[]; + @Input() isModal?: boolean; // Whether the component is loaded in a modal. + @Input() manage?: boolean; + @Input() pick?: boolean; // To pick a file you MUST use a modal. + @Input() path?: string; + @Input() showSitePicker?: boolean; + @Output() onPathChanged = new EventEmitter(); + @Output() onFilePicked = new EventEmitter(); + + filesLoaded = false; + files?: (FileEntry | DirectoryEntry)[]; + + protected shareObserver?: CoreEventObserver; + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.siteId = this.siteId || CoreSites.getCurrentSiteId(); + + this.loadFiles(); + + // Listen for new files shared with the app. + this.shareObserver = CoreEvents.on(CoreEvents.FILE_SHARED, (data) => { + if (data.siteId == this.siteId) { + // File was stored in current site, refresh the list. + this.filesLoaded = false; + this.loadFiles().finally(() => { + this.filesLoaded = true; + }); + } + }); + } + + /** + * Load the files. + * + * @return Promise resolved when done. + */ + protected async loadFiles(): Promise { + this.files = await CoreSharedFiles.getSiteSharedFiles(this.siteId, this.path, this.mimetypes); + this.filesLoaded = true; + } + + /** + * Refresh the list of files. + * + * @param refresher Refresher. + */ + refreshFiles(refresher: IonRefresher): void { + this.loadFiles().finally(() => { + refresher.complete(); + }); + } + + /** + * Called when a file is deleted. Remove the file from the list. + * + * @param index Position of the file. + */ + fileDeleted(index: number): void { + this.files!.splice(index, 1); + } + + /** + * Called when a file is renamed. Update the list. + * + * @param index Position of the file. + * @param data Data containing the new FileEntry. + */ + fileRenamed(index: number, data: { file: FileEntry }): void { + this.files![index] = data.file; + } + + /** + * Open a subfolder. + * + * @param folder The folder to open. + */ + openFolder(folder: DirectoryEntry): void { + const path = CoreTextUtils.concatenatePaths(this.path || '', folder.name); + + if (this.isModal) { + this.path = path; + this.filesLoaded = false; + this.loadFiles(); + this.onPathChanged.emit(path); + + return; + } + + const hash = Md5.hashAsciiStr(path); + + CoreNavigator.navigate(`../${hash}`, { + params: { + path, + manage: this.manage, + pick: this.pick, + siteId: this.siteId, + mimetypes: this.mimetypes, + isModal: false, + }, + }); + } + + /** + * Change site loaded. + * + * @param id Site to load. + */ + changeSite(id: string): void { + this.siteId = id; + this.path = ''; + this.filesLoaded = false; + this.loadFiles(); + this.onPathChanged.emit(''); + } + + /** + * A file was picked. + * + * @param file Picked file. + */ + filePicked(file: FileEntry): void { + this.onFilePicked.emit(file); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.shareObserver?.off(); + } + +} diff --git a/src/core/features/sharedfiles/pages/choose-site/choose-site.html b/src/core/features/sharedfiles/pages/choose-site/choose-site.html new file mode 100644 index 000000000..a37711cad --- /dev/null +++ b/src/core/features/sharedfiles/pages/choose-site/choose-site.html @@ -0,0 +1,32 @@ + + + + + + {{ 'core.sharedfiles.sharedfiles' | translate }} + + + + + + + +

{{ 'core.sharedfiles.chooseaccountstorefile' | translate }}

+

{{fileName}}

+
+
+ + + +

{{site.fullName}}

+

+

{{site.siteUrl}}

+
+
+
+
+
diff --git a/src/core/features/sharedfiles/pages/choose-site/choose-site.ts b/src/core/features/sharedfiles/pages/choose-site/choose-site.ts new file mode 100644 index 000000000..a802e1efa --- /dev/null +++ b/src/core/features/sharedfiles/pages/choose-site/choose-site.ts @@ -0,0 +1,108 @@ +// (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 } from '@angular/core'; +import { CoreSharedFilesHelper } from '@features/sharedfiles/services/sharedfiles-helper'; +import { FileEntry } from '@ionic-native/file'; +import { CoreFile } from '@services/file'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Page to display the list of sites to choose one to store a shared file. + */ +@Component({ + selector: 'page-core-shared-files-choose-site', + templateUrl: 'choose-site.html', +}) +export class CoreSharedFilesChooseSitePage implements OnInit { + + fileName?: string; + sites?: CoreSiteBasicInfo[]; + loaded = false; + + protected filePath?: string; + protected fileEntry?: FileEntry; + protected isInbox = false; // Whether the file is in the Inbox folder. + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.filePath = CoreNavigator.getRouteParam('filePath'); + this.isInbox = !!CoreNavigator.getRouteBooleanParam('isInbox'); + + if (!this.filePath) { + CoreDomUtils.showErrorModal('Error reading file.'); + await CoreUtils.nextTick(); + CoreNavigator.back(); + + return; + } + + const fileAndDir = CoreFile.getFileAndDirectoryFromPath(this.filePath); + this.fileName = fileAndDir.name; + + try { + await Promise.all([ + this.loadFile(), + this.loadSites(), + ]); + } catch { + CoreDomUtils.showErrorModal('Error reading file.'); + CoreNavigator.back(); + } finally { + this.loaded = true; + } + } + + /** + * Load the file data. + * + * @return Promise resolved when done. + */ + protected async loadFile(): Promise { + this.fileEntry = await CoreFile.getExternalFile(this.filePath!); + this.fileName = this.fileEntry.name; + } + + /** + * Load sites. + * + * @return Promise resolved when done. + */ + protected async loadSites(): Promise { + this.sites = await CoreSites.getSites(); + } + + /** + * Store the file in a certain site. + * + * @param siteId Site ID. + */ + async storeInSite(siteId: string): Promise { + this.loaded = false; + + try { + await CoreSharedFilesHelper.storeSharedFileInSite(this.fileEntry!, siteId, this.isInbox); + + CoreNavigator.back(); + } finally { + this.loaded = true; + } + } + +} diff --git a/src/core/features/sharedfiles/pages/list/list.html b/src/core/features/sharedfiles/pages/list/list.html new file mode 100644 index 000000000..386c0ad7d --- /dev/null +++ b/src/core/features/sharedfiles/pages/list/list.html @@ -0,0 +1,13 @@ + + + + + + {{ title }} + + + + + + diff --git a/src/core/features/sharedfiles/pages/list/list.ts b/src/core/features/sharedfiles/pages/list/list.ts new file mode 100644 index 000000000..2525cd2db --- /dev/null +++ b/src/core/features/sharedfiles/pages/list/list.ts @@ -0,0 +1,63 @@ +// (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 } from '@angular/core'; + +import { CoreFile } from '@services/file'; +import { CoreNavigator } from '@services/navigator'; +import { Translate } from '@singletons'; + +/** + * Page to display the list of shared files. + */ +@Component({ + selector: 'page-core-shared-files-list', + templateUrl: 'list.html', +}) +export class CoreSharedFilesListPage implements OnInit { + + siteId?: string; + mimetypes?: string[]; + manage = false; + showSitePicker = false; + path = ''; + title?: string; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.siteId = CoreNavigator.getRouteParam('siteId'); + this.mimetypes = CoreNavigator.getRouteParam('mimetypes'); + this.manage = !!CoreNavigator.getRouteBooleanParam('manage'); + this.path = CoreNavigator.getRouteParam('path') || ''; + this.showSitePicker = !CoreNavigator.getRouteParam('hideSitePicker'); + + this.calculateTitle(this.path); + } + + /** + * Calculate the title. + * + * @param path Path to use. + */ + calculateTitle(path?: string): void { + if (path) { + this.title = CoreFile.getFileAndDirectoryFromPath(path).name; + } else { + this.title = Translate.instant('core.sharedfiles.sharedfiles'); + } + } + +} diff --git a/src/core/features/sharedfiles/sharedfiles-lazy.module.ts b/src/core/features/sharedfiles/sharedfiles-lazy.module.ts new file mode 100644 index 000000000..878a53cf9 --- /dev/null +++ b/src/core/features/sharedfiles/sharedfiles-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 { CoreSharedFilesComponentsModule } from './components/components.module'; +import { CoreSharedFilesListPage } from './pages/list/list'; +import { CoreSharedFilesChooseSitePage } from './pages/choose-site/choose-site'; + +const routes: Routes = [ + { + path: 'choosesite', + component: CoreSharedFilesChooseSitePage, + }, + { + path: 'list/:hash', + component: CoreSharedFilesListPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreSharedFilesComponentsModule, + ], + declarations: [ + CoreSharedFilesListPage, + CoreSharedFilesChooseSitePage, + ], +}) +export class CoreSharedFilesLazyModule {} diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index bd223c89d..3df8f454b 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -465,6 +465,21 @@ export class CoreNavigatorService { return 'param-' + (++this.lastParamId); } + /** + * Replace the route params in a path with the params values. + * + * @param path Path. + * @param params Params. + * @returns Path with params replaced. + */ + replaceRoutePathParams(path: string, params?: Params): string { + for (const name in params) { + path = path.replace(`:${name}`, params[name]); + } + + return path; + } + } export const CoreNavigator = makeSingleton(CoreNavigatorService); diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 8b433b74e..21c12f149 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -373,6 +373,6 @@ export type CoreEventFileSharedData = { /** * Data passed to APP_LAUNCHED_URL event. */ - export type CoreEventAppLaunchedData = { +export type CoreEventAppLaunchedData = { url: string; };