Skip to content

Commit

Permalink
MOBILE-2256 privatefiles: Remove private files
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonso-salces committed Sep 4, 2024
1 parent 3fdc860 commit 03cee5a
Show file tree
Hide file tree
Showing 12 changed files with 368 additions and 15 deletions.
3 changes: 3 additions & 0 deletions scripts/langindex.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions src/addons/privatefiles/components/file-actions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<header>
<ion-item button="false" detail="false" class="ion-text-wrap">
<ion-thumbnail slot="start">
<img [src]="icon" alt="" role="presentation" />
</ion-thumbnail>

<ion-label>{{ filename }}</ion-label>

<ion-button shape="round" size="default" slot="end" fill="clear" [ariaLabel]="'core.close' | translate"
(click)="close({ status: 'cancel' })">
<ion-icon slot="icon-only" name="close" aria-label="hidden" />
</ion-button>
</ion-item>
</header>

<hr>

<ion-list>
<ion-item button="false" detail="false" lines="none">
<ion-icon slot="start" name="fas-cloud" aria-hidden="true" />
<ion-label>{{ 'addon.privatefiles.availableoffline' | translate }}</ion-label>
<ion-toggle slot="end" [(ngModel)]="toggleValue" (ngModelChange)="close({ status: toggleValue ? 'download' : 'deleteOffline' })" />
</ion-item>

<ion-item detail="false" (click)="close({ status: 'deleteOnline' })" lines="none">
<ion-icon slot="start" name="fas-trash" aria-hidden="true" color="danger" />
<ion-label> {{ 'core.delete' | translate }} </ion-label>
</ion-item>
</ion-list>
8 changes: 8 additions & 0 deletions src/addons/privatefiles/components/file-actions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
hr {
background: var(--gray-300);
}

ion-thumbnail {
--size: 1.5rem;
margin-inline-end: 0.5rem;
}
51 changes: 51 additions & 0 deletions src/addons/privatefiles/components/file-actions.ts
Original file line number Diff line number Diff line change
@@ -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<AddonPrivateFilesFileActionsComponentParams>
implements OnInit {

@Input({ required: true }) selected = false;
@Input({ required: true }) filename = '';
@Input({ required: true }) icon = '';

toggleValue = false;

constructor(elementRef: ElementRef<HTMLElement>) {
super(elementRef);
}

/**
* @inheritdoc
*/
ngOnInit(): void {
this.toggleValue = this.selected;
}

}

export type AddonPrivateFilesFileActionsComponentParams = {
status: 'cancel' | 'deleteOnline' | 'deleteOffline' | 'download';
};
5 changes: 4 additions & 1 deletion src/addons/privatefiles/lang.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?"
}
25 changes: 22 additions & 3 deletions src/addons/privatefiles/pages/index/index.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
@if (selectFilesEnabled()) {
<ion-button fill="clear" [ariaLabel]="'core.close' | translate" (click)="cancelFileSelection()">
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" />
</ion-button>
} @else {
<ion-back-button [text]="'core.back' | translate" />
}
</ion-buttons>
<ion-title>
<h1>{{ title }}</h1>
<h1>{{ selectFilesEnabled() ? (selectedFiles.length + ' ' + title) : title }}</h1>
</ion-title>
<ion-buttons slot="end">
@if (selectFilesEnabled()) {
<ion-button fill="clear" (click)="deleteFiles()" [ariaLabel]="'core.delete' | translate" color="danger">
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true" />
</ion-button>
}
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="limited-width">
Expand All @@ -28,6 +41,7 @@ <h1>{{ title }}</h1>
<!-- Display info about space used and space left. -->
<ion-card class="core-info-card" *ngIf="userQuota && filesInfo && filesInfo.filecount > 0">
<ion-item>
<ion-icon slot="start" aria-label="hidden" name="fas-cloud" />
<ion-label>
{{ 'core.quotausage' | translate:{$a: {used: spaceUsed, total: userQuotaReadable} } }}
</ion-label>
Expand All @@ -42,7 +56,10 @@ <h1>{{ title }}</h1>
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.folder' | translate" />
<ion-label>{{file.filename}}</ion-label>
</ion-item>
<core-file *ngIf="!file.isdir" [file]="file" [component]="component" [componentId]="file.contextid" />
<core-file *ngIf="!file.isdir" [file]="file" [component]="component" [componentId]="file.contextid"
(onOpenMenuClick)="openManagementFileMenu(file)" (longPress)="selectFilesEnabled.set(true)"
[downloadState]="file.downloadState" [showCheckbox]="selectFilesEnabled()"
(onCheckboxChange)="checkboxValueChanged($event, file)" showDownloadStatus="true" canDownload="false" />
</ng-container>
</ion-list>

Expand All @@ -51,10 +68,12 @@ <h1>{{ title }}</h1>
</core-loading>

<!-- Upload a private file. -->
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="showUpload && root !== 'site' && !path">
@if (showUpload && root !== 'site' && !path && !selectFilesEnabled()) {
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="uploadFile()" [attr.aria-label]="'core.fileuploader.uploadafile' | translate">
<ion-icon name="fas-plus" aria-hidden="true" />
<span class="sr-only">{{ 'core.fileuploader.uploadafile' | translate }}</span>
</ion-fab-button>
</ion-fab>
}
</ion-content>
171 changes: 170 additions & 1 deletion src/addons/privatefiles/pages/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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<void> {
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<void> {
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<void> {
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();
}
}

}
Loading

0 comments on commit 03cee5a

Please sign in to comment.