diff --git a/scripts/langindex.json b/scripts/langindex.json
index c741bd67b51..2a652250c88 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1141,10 +1141,13 @@
"addon.notifications.therearentnotificationsyet": "local_moodlemobileapp",
"addon.notifications.typeofnotification": "local_moodlemobileapp",
"addon.notifications.unreadnotification": "message",
+ "addon.privatefiles.availableoffline": "local_moodlemobileapp",
+ "addon.privatefiles.confirmremoveselectedfiles": "local_moodlemobileapp",
"addon.privatefiles.couldnotloadfiles": "local_moodlemobileapp",
"addon.privatefiles.emptyfilelist": "local_moodlemobileapp",
"addon.privatefiles.erroruploadnotworking": "local_moodlemobileapp",
"addon.privatefiles.files": "moodle",
+ "addon.privatefiles.filedeletedsuccessfully": "local_moodlemobileapp",
"addon.privatefiles.privatefiles": "moodle",
"addon.privatefiles.sitefiles": "moodle",
"addon.qtype_essay.maxwordlimitboundary": "qtype_essay",
diff --git a/src/addons/privatefiles/components/file-actions.html b/src/addons/privatefiles/components/file-actions.html
new file mode 100644
index 00000000000..7532a7539ec
--- /dev/null
+++ b/src/addons/privatefiles/components/file-actions.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ {{ filename }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'addon.privatefiles.availableoffline' | translate }}
+
+
+
+
+
+ {{ 'core.delete' | translate }}
+
+
diff --git a/src/addons/privatefiles/components/file-actions.scss b/src/addons/privatefiles/components/file-actions.scss
new file mode 100644
index 00000000000..da555c7bcfe
--- /dev/null
+++ b/src/addons/privatefiles/components/file-actions.scss
@@ -0,0 +1,8 @@
+hr {
+ background: var(--gray-300);
+}
+
+ion-thumbnail {
+ --size: 1.5rem;
+ margin-inline-end: 0.5rem;
+}
diff --git a/src/addons/privatefiles/components/file-actions.ts b/src/addons/privatefiles/components/file-actions.ts
new file mode 100644
index 00000000000..edd8d6913ee
--- /dev/null
+++ b/src/addons/privatefiles/components/file-actions.ts
@@ -0,0 +1,51 @@
+// (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 { CoreSharedModule } from '@/core/shared.module';
+import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core';
+import { CoreModalComponent } from '@classes/modal-component';
+
+@Component({
+ selector: 'core-privatefiles-file-actions',
+ styleUrl: './file-actions.scss',
+ templateUrl: 'file-actions.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ imports: [CoreSharedModule],
+})
+export class AddonPrivateFilesFileActionsComponent extends CoreModalComponent
+ implements OnInit {
+
+ @Input({ required: true }) selected = false;
+ @Input({ required: true }) filename = '';
+ @Input({ required: true }) icon = '';
+
+ toggleValue = false;
+
+ constructor(elementRef: ElementRef) {
+ super(elementRef);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ ngOnInit(): void {
+ this.toggleValue = this.selected;
+ }
+
+}
+
+export type AddonPrivateFilesFileActionsComponentParams = {
+ status: 'cancel' | 'deleteOnline' | 'deleteOffline' | 'download';
+};
diff --git a/src/addons/privatefiles/lang.json b/src/addons/privatefiles/lang.json
index 585d7684db6..c64c157d493 100644
--- a/src/addons/privatefiles/lang.json
+++ b/src/addons/privatefiles/lang.json
@@ -4,5 +4,8 @@
"erroruploadnotworking": "Unfortunately it is currently not possible to upload files to your site.",
"files": "Files",
"privatefiles": "Private files",
- "sitefiles": "Site files"
+ "sitefiles": "Site files",
+ "filedeletedsuccessfully": "You have deleted {{filename}} succesfully",
+ "availableoffline": "Available offline",
+ "confirmremoveselectedfiles": "This will permanently delete selected files. Are you sure about this action?"
}
diff --git a/src/addons/privatefiles/pages/index/index.html b/src/addons/privatefiles/pages/index/index.html
index 14080063a6d..77f07593635 100644
--- a/src/addons/privatefiles/pages/index/index.html
+++ b/src/addons/privatefiles/pages/index/index.html
@@ -1,11 +1,24 @@
+ @if (selectFilesEnabled()) {
+
+
+
+ } @else {
+ }
- {{ title }}
+ {{ selectFilesEnabled() ? (selectedFiles.length + ' ' + title) : title }}
+
+ @if (selectFilesEnabled()) {
+
+
+
+ }
+
@@ -28,6 +41,7 @@ {{ title }}
0">
+
{{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }}
@@ -42,7 +56,10 @@ {{ title }}
{{file.filename}}
-
+
@@ -51,10 +68,12 @@ {{ title }}
-
+ @if (showUpload && root !== 'site' && !path && !selectFilesEnabled()) {
+
{{ 'core.fileuploader.uploadafile' | translate }}
+ }
diff --git a/src/addons/privatefiles/pages/index/index.ts b/src/addons/privatefiles/pages/index/index.ts
index 0dc3cc7202c..dd1e087411f 100644
--- a/src/addons/privatefiles/pages/index/index.ts
+++ b/src/addons/privatefiles/pages/index/index.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit, signal } from '@angular/core';
import { Md5 } from 'ts-md5/dist/md5';
import { CoreNetwork } from '@services/network';
@@ -33,6 +33,14 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
+import { CoreLoadings } from '@services/loadings';
+import { CoreIonLoadingElement } from '@classes/ion-loading';
+import { CoreFile } from '@services/file';
+import { CoreModals } from '@services/modals';
+import { CoreFilepool } from '@services/filepool';
+import { CoreFileHelper } from '@services/file-helper';
+import { CoreToasts, ToastDuration } from '@services/toasts';
+import { CoreMimetypeUtils } from '@services/utils/mimetype';
/**
* Page that displays the list of files.
@@ -56,6 +64,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
files?: AddonPrivateFilesFile[]; // List of files.
component!: string; // Component to link the file downloads to.
filesLoaded = false; // Whether the files are loaded.
+ selectFilesEnabled = signal(false);
+ selectedFiles: AddonPrivateFilesFile[] = [];
protected updateSiteObserver: CoreEventObserver;
protected logView: () => void;
@@ -279,4 +289,163 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
this.updateSiteObserver?.off();
}
+ /**
+ * Delete a private file.
+ *
+ * @param file File to remove.
+ * @param showLoading Show loading.
+ */
+ async deleteFile(file: AddonPrivateFilesFile, showLoading = true): Promise {
+ let loading: CoreIonLoadingElement | undefined = undefined;
+
+ if (showLoading) {
+ loading = await CoreLoadings.show();
+ }
+
+ try {
+ await AddonPrivateFiles.deleteFile(file);
+ await this.refreshFiles();
+ } catch (error) {
+ await CoreDomUtils.showErrorModalDefault(error, 'An error occourred while file was being deleted.');
+ }
+
+ if (loading) {
+ loading.dismiss();
+ }
+ }
+
+ /**
+ * Delete private files.
+ */
+ async deleteFiles(): Promise {
+ try {
+ await CoreDomUtils.showDeleteConfirm('addon.privatefiles.confirmremoveselectedfiles');
+ } catch (error) {
+ if (!CoreDomUtils.isCanceledError(error)) {
+ throw error;
+ }
+
+ return;
+ }
+
+ const loading = await CoreLoadings.show();
+
+ for (const file of this.selectedFiles) {
+ await this.deleteFile(file, false);
+ }
+
+ loading.dismiss();
+
+ const message = Translate.instant(
+ 'addon.privatefiles.filedeletedsuccessfully',
+ { filename: this.selectedFiles.length + ' ' + Translate.instant('addon.privatefiles.files') },
+ );
+
+ this.selectedFiles = [];
+ this.selectFilesEnabled.set(false);
+ await CoreToasts.show({ message, translateMessage: false, duration: ToastDuration.SHORT });
+ }
+
+ /**
+ * File selection changes.
+ *
+ * @param event selection value.
+ * @param file File selection.
+ */
+ checkboxValueChanged(event: boolean, file: AddonPrivateFilesFile): void {
+ if (event) {
+ this.selectedFiles.push(file);
+
+ return;
+ }
+
+ this.selectedFiles = this.selectedFiles.filter(selectedFile => selectedFile !== file);
+ }
+
+ /**
+ * Cancel file selection.
+ */
+ cancelFileSelection(): void {
+ this.selectFilesEnabled.set(false);
+ this.selectedFiles = [];
+ }
+
+ /**
+ * Open file management menu.
+ *
+ * @param file File to manage.
+ * @returns Promise done.
+ */
+ async openManagementFileMenu(file: AddonPrivateFilesFile): Promise {
+ const siteId = CoreSites.getCurrentSiteId();
+ const fileState = await CoreFilepool.getFileStateByUrl(siteId, file.fileurl, file.timemodified);
+ const isFileDownloaded = CoreFileHelper.isStateDownloaded(fileState);
+ const { AddonPrivateFilesFileActionsComponent } = await import('@addons/privatefiles/components/file-actions');
+
+ const icon = file.mimetype
+ ? CoreMimetypeUtils.getMimetypeIcon(file.mimetype)
+ : CoreMimetypeUtils.getFileIcon(file.filename);
+
+ const { status } = await CoreModals.openSheet(
+ AddonPrivateFilesFileActionsComponent,
+ { selected: isFileDownloaded, filename: file.filename, icon },
+ );
+
+ if (status === 'cancel') {
+ return;
+ }
+
+ const loading = await CoreLoadings.show();
+
+ if (status === 'deleteOnline') {
+ try {
+ await CoreDomUtils.showDeleteConfirm('core.confirmdeletefile');
+ } catch (error) {
+ if (!CoreDomUtils.isCanceledError(error)) {
+ throw error;
+ }
+
+ return;
+ }
+
+ await this.deleteFile(file);
+ const message = Translate.instant('addon.privatefiles.filedeletedsuccessfully', { filename: file.filename });
+ await CoreToasts.show({ message, translateMessage: false, duration: ToastDuration.SHORT });
+
+ return loading.dismiss();
+ }
+
+ if (status === 'deleteOffline') {
+ try {
+ const filePath = await CoreFilepool.getFilePathByUrl(siteId, file.fileurl);
+ await CoreFile.removeFile(filePath);
+ file.downloadState = undefined;
+ } catch (error) {
+ CoreDomUtils.showErrorModalDefault(error, 'core.errordeletefile', true);
+ }
+
+ return loading.dismiss();
+ }
+
+ try {
+ await CoreFilepool.addToQueueByUrl(
+ siteId,
+ CoreFileHelper.getFileUrl(file),
+ this.component,
+ file.contextid,
+ file.timemodified,
+ undefined,
+ undefined,
+ 0,
+ file,
+ );
+
+ file.downloadState = 'downloaded';
+ } catch (error) {
+ CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ } finally {
+ loading.dismiss();
+ }
+ }
+
}
diff --git a/src/addons/privatefiles/services/privatefiles.ts b/src/addons/privatefiles/services/privatefiles.ts
index c638bda9c2f..58843df724c 100644
--- a/src/addons/privatefiles/services/privatefiles.ts
+++ b/src/addons/privatefiles/services/privatefiles.ts
@@ -20,6 +20,8 @@ import { CoreWSExternalWarning } from '@services/ws';
import { CoreSite } from '@classes/sites/site';
import { makeSingleton } from '@singletons';
import { ContextLevel } from '@/core/constants';
+import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
+import { CoreFilepool } from '@services/filepool';
const ROOT_CACHE_KEY = 'mmaFiles:';
@@ -92,8 +94,10 @@ export class AddonPrivateFilesProvider {
return [];
}
- return result.files.map((entry) => {
+ return await Promise.all(result.files.map(async (entry) => {
entry.fileurl = entry.url;
+ const fileState = await CoreFilepool.getFileStateByUrl(site.id, entry.fileurl, entry.timemodified);
+ entry.downloadState = fileState;
if (entry.isdir) {
entry.imgPath = CoreMimetypeUtils.getFolderIcon();
@@ -102,7 +106,7 @@ export class AddonPrivateFilesProvider {
}
return entry;
- });
+ }));
}
@@ -388,6 +392,22 @@ export class AddonPrivateFilesProvider {
return site.write('core_user_add_user_private_files', params, preSets);
}
+ /**
+ * Delete a private file.
+ *
+ * @param file Private file to remove.
+ * @param siteId Site ID.
+ */
+ async deleteFile({ filename, filepath }: AddonPrivateFilesFile, siteId?: string): Promise {
+ const site = await CoreSites.getSite(siteId);
+ const { draftitemid } = await site.write(
+ 'core_user_prepare_private_files_for_edition',
+ {},
+ );
+ await CoreFileUploader.deleteDraftFiles(draftitemid, [{ filename, filepath }]);
+ await site.write('core_user_update_private_files', { draftitemid });
+ }
+
}
export const AddonPrivateFiles = makeSingleton(AddonPrivateFilesProvider);
@@ -409,6 +429,8 @@ export type AddonPrivateFilesFile = {
filesize?: number; // File size.
author?: string; // File owner.
license?: string; // File license.
+ downloadState?: string;
+ mimetype?: string;
} & AddonPrivateFilesFileCalculatedData;
/**
@@ -472,3 +494,12 @@ export type AddonPrivateFilesGetUserInfoWSResult = {
type AddonPrivateFilesAddUserPrivateFilesWSParams = {
draftid: number; // Draft area id.
};
+
+/**
+ * Body of core_user_prepare_private_files_for_edition WS response.
+ */
+type AddonPrivateFilesPreparePrivateFilesForEditionResponse = {
+ areaoptions: { name: string; value: string | number }[];
+ draftitemid: number;
+ warnings?: CoreWSExternalWarning[];
+};
diff --git a/src/core/components/file/core-file.html b/src/core/components/file/core-file.html
index e053a2fbaa9..1eb3f250781 100644
--- a/src/core/components/file/core-file.html
+++ b/src/core/components/file/core-file.html
@@ -1,10 +1,26 @@
-
-
+
+
+ @if (showCheckbox) {
+
+ } @else {
+
-
- {{fileName}}
+ }
+
+
+
+ {{fileName}}
+
+ @if (downloadState === 'downloaded') {
+
+ }
+
+
+
{{ fileSizeReadable }}
ยท
@@ -19,10 +35,16 @@
-
+ @if (!canDelete && !showCheckbox) {
+
+
+
+ } @else if (!isDownloading && canDelete && !isDownloaded) {
+
+ }
+
diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts
index 574a138e2ef..c34ab74cb54 100644
--- a/src/core/components/file/file.ts
+++ b/src/core/components/file/file.ts
@@ -47,7 +47,11 @@ export class CoreFileComponent implements OnInit, OnDestroy {
@Input({ transform: toBoolean }) canDownload = true; // Whether file can be downloaded.
@Input({ transform: toBoolean }) showSize = true; // Whether show filesize.
@Input({ transform: toBoolean }) showTime = true; // Whether show file time modified.
+ @Input({ transform: toBoolean }) showCheckbox = true; // Show checkbox
+ @Input({ required: false }) downloadState?: string; // Download state.
@Output() onDelete: EventEmitter; // Will notify when the delete button is clicked.
+ @Output() onCheckboxChange: EventEmitter; // Will notify when the checkbox value changes.
+ @Output() onOpenMenuClick: EventEmitter; // Will notify when menu clicked.
isDownloading?: boolean;
isDownloaded?: boolean;
@@ -59,6 +63,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
isIOS = false;
openButtonIcon = '';
openButtonLabel = '';
+ selected = false;
protected fileUrl!: string;
protected siteId?: string;
@@ -68,6 +73,8 @@ export class CoreFileComponent implements OnInit, OnDestroy {
constructor() {
this.onDelete = new EventEmitter();
+ this.onCheckboxChange = new EventEmitter();
+ this.onOpenMenuClick = new EventEmitter();
}
/**
diff --git a/src/core/services/modals.ts b/src/core/services/modals.ts
index 13df097d737..0434273d3c5 100644
--- a/src/core/services/modals.ts
+++ b/src/core/services/modals.ts
@@ -69,13 +69,13 @@ export class CoreModalsService {
* @param component Component to render inside the modal.
* @returns Modal result once it's been closed.
*/
- async openSheet(component: Constructor>): Promise {
+ async openSheet(component: Constructor>, componentProps: Record = {}): Promise {
const container = document.querySelector('ion-app') ?? document.body;
const viewContainer = container.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
const element = await AngularFrameworkDelegate.attachViewToDom(
container,
CoreSheetModalComponent,
- { component },
+ { component, componentProps },
);
const sheetModal = CoreDirectivesRegistry.require>>(
element,
diff --git a/src/theme/components/ion-item.scss b/src/theme/components/ion-item.scss
index b4fca69e709..6f76bc4d84d 100644
--- a/src/theme/components/ion-item.scss
+++ b/src/theme/components/ion-item.scss
@@ -229,6 +229,17 @@ ion-item.item.item-file {
cursor: pointer;
}
+ ion-checkbox {
+ flex: none;
+ width: 3rem;
+ }
+
+ &.file-selected {
+ --ion-item-background: var(--primary-tint);
+ }
+
+ --inner-border-width: 0 !important;
+
}
.item-dimmed {