diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index 062c160c5fe4f3fa84539d5e9665a2b5c299f5e5..ed942d2b4006a1d7be4b27325465d2089b8d506e 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -16,6 +16,7 @@ import * as samp from './store/reducers/samp.reducer'; import * as coneSearch from './store/reducers/cone-search.reducer'; import * as detail from './store/reducers/detail.reducer'; import * as svomJsonKw from './store/reducers/svom-json-kw.reducer'; +import * as downloadFile from './store/reducers/download-file.reducer'; /** * Interface for instance state. @@ -28,7 +29,8 @@ export interface State { samp: samp.State, coneSearch: coneSearch.State detail: detail.State, - svomJsonKw: svomJsonKw.State + svomJsonKw: svomJsonKw.State, + downloadFile: downloadFile.State } const reducers = { @@ -37,7 +39,8 @@ const reducers = { samp: samp.sampReducer, coneSearch: coneSearch.coneSearchReducer, detail: detail.detailReducer, - svomJsonKw: svomJsonKw.svomJsonKwReducer + svomJsonKw: svomJsonKw.svomJsonKwReducer, + downloadFile: downloadFile.fileReducer }; export const instanceReducer = combineReducers(reducers); diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index b0ad6b0c98a437955dc1be5f759df0ce1f640a94..c638c569a6788c2fcad9652d1aa1ee2d7b1c1439 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -33,7 +33,8 @@ [selectedData]="selectedData" (retrieveData)="retrieveData.emit($event)" (addSelectedData)="addSelectedData.emit($event)" - (deleteSelectedData)="deleteSelectedData.emit($event)"> + (deleteSelectedData)="deleteSelectedData.emit($event)" + (startsDownloadingFile)="startsDownloadingFile.emit($event)"> </app-datatable> </accordion-group> </accordion> diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index c70564ba46caa5d342e2c80c5516221d549dab58..4eee5ec45290fe4ca2bc8e3dfe433d55ad38c55b 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -40,4 +40,5 @@ export class DatatableTabComponent { @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() broadcast: EventEmitter<string> = new EventEmitter(); + @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); } diff --git a/client/src/app/instance/search/components/result/datatable.component.html b/client/src/app/instance/search/components/result/datatable.component.html index bbf61801f4fcfe35d312fcebf46867356116be76..f4b0078f47282cd65be076a86f85df27351ce027 100644 --- a/client/src/app/instance/search/components/result/datatable.component.html +++ b/client/src/app/instance/search/components/result/datatable.component.html @@ -62,7 +62,8 @@ [value]="datum[attribute.label]" [datasetName]="dataset.name" [datasetPublic]="dataset.public" - [config]="getRendererConfig(attribute)"> + [config]="getRendererConfig(attribute)" + (startsDownloadingFile)="startsDownloadingFile.emit($event)"> </app-download-renderer> </div> <div *ngSwitchCase="'image'"> diff --git a/client/src/app/instance/search/components/result/datatable.component.ts b/client/src/app/instance/search/components/result/datatable.component.ts index bab2e9c71b6b4105fa0e39b3c018bee5f042e6a7..33191d25025d37bc79730b712355c1ae7fa59f5a 100644 --- a/client/src/app/instance/search/components/result/datatable.component.ts +++ b/client/src/app/instance/search/components/result/datatable.component.ts @@ -44,6 +44,7 @@ export class DatatableComponent implements OnInit { @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); public page = 1; public nbItems = 10; diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.html b/client/src/app/instance/search/components/result/download-file-tab.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2030ad73e5a57d05676fc9ced77a03b14cecfe99 --- /dev/null +++ b/client/src/app/instance/search/components/result/download-file-tab.component.html @@ -0,0 +1,11 @@ +<div class="jumbotron mb-4 py-4"> + <div class="lead"> + Files downloaded : + <ul> + <li *ngFor="let downloadFile of downloadedFiles"> + {{ downloadFile.name }} : + <progressbar [value]="downloadFile.progress" type="warning" [striped]="false">{{ downloadFile.progress }}%</progressbar> + </li> + </ul> + </div> +</div> \ No newline at end of file diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.scss b/client/src/app/instance/search/components/result/download-file-tab.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/app/instance/search/components/result/download-file-tab.component.ts b/client/src/app/instance/search/components/result/download-file-tab.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc8ad76cea0da902fd115c06c7159e8a626bb6ed --- /dev/null +++ b/client/src/app/instance/search/components/result/download-file-tab.component.ts @@ -0,0 +1,25 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input } from '@angular/core'; + +import { DownloadFile } from 'src/app/instance/store/models'; + +/** + * @class + * @classdesc Search result reminder component. + */ +@Component({ + selector: 'app-download-file-tab', + templateUrl: 'download-file-tab.component.html', + styleUrls: ['download-file-tab.component.scss'] +}) +export class DownloadFileTabComponent { + @Input() downloadedFiles: DownloadFile[]; +} diff --git a/client/src/app/instance/search/components/result/download.component.ts b/client/src/app/instance/search/components/result/download.component.ts index 6094323b74b8711022be8e8b0c92059b17f1d8cb..4b3c199ac2a5fc7ed474960ee75d51986118759a 100644 --- a/client/src/app/instance/search/components/result/download.component.ts +++ b/client/src/app/instance/search/components/result/download.component.ts @@ -8,7 +8,7 @@ */ import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpRequest, HttpEventType } from '@angular/common/http'; import { interval, Subscription} from 'rxjs'; import { Criterion, ConeSearch, criterionToString } from '../../../store/models'; @@ -33,6 +33,7 @@ export class DownloadComponent implements OnDestroy { @Input() dataLength: number; @Input() sampRegistered: boolean; @Output() broadcast: EventEmitter<string> = new EventEmitter(); + @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); archiveName = ''; archiveInProgress = false; @@ -93,19 +94,15 @@ export class DownloadComponent implements OnDestroy { } /** - * Returns URL to download archive. - * - * @return boolean + * Allows to download file. */ - createFilesArchiveUrl(): string { - let query: string = `${getHost(this.appConfig.apiUrl)}/archive/${this.datasetSelected}?a=${this.outputList.join(';')}`; - if (this.criteriaList.length > 0) { - query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')}`; - } - if (this.coneSearch) { - query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; - } - return query; + click(event, href, extension): void { + event.preventDefault(); + + const url = href; + const filename = `${this.datasetSelected}.${extension}`; + + this.startsDownloadingFile.emit({ url, filename }) } /** @@ -113,24 +110,24 @@ export class DownloadComponent implements OnDestroy { * * @fires EventEmitter<string> */ - broadcastVotable(): void { + broadcastVotable(): void { this.broadcast.emit(this.getUrl('votable')); } /** - * Allows to download file. + * Returns URL to download archive. + * + * @return boolean */ - click(event, href, extension): void { - event.preventDefault(); - - this.http.get(href, {responseType: "blob"}).subscribe( - data => { - let downloadLink = document.createElement('a'); - downloadLink.href = window.URL.createObjectURL(data); - downloadLink.setAttribute('download', `${this.datasetSelected}.${extension}`); - downloadLink.click(); - } - ); + createFilesArchiveUrl(): string { + let query: string = `${getHost(this.appConfig.apiUrl)}/archive/${this.datasetSelected}?a=${this.outputList.join(';')}`; + if (this.criteriaList.length > 0) { + query += `&c=${this.criteriaList.map(criterion => criterionToString(criterion)).join(';')}`; + } + if (this.coneSearch) { + query += `&cs=${this.coneSearch.ra}:${this.coneSearch.dec}:${this.coneSearch.radius}`; + } + return query; } getArchiveUrl() { diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts index fffbcf4e766b53c4b4788371155f5e08e7b22207..4fa8d96abe8417aebb5a490ea60e9744f7616cc6 100644 --- a/client/src/app/instance/search/components/result/index.ts +++ b/client/src/app/instance/search/components/result/index.ts @@ -5,6 +5,7 @@ import { SampComponent } from './samp.component'; import { UrlDisplayComponent } from './url-display.component'; import {Â DatatableComponent } from './datatable.component'; import { DatatableActionsComponent } from './datatable-actions.component'; +import { DownloadFileTabComponent } from './download-file-tab.component'; import { rendererComponents } from './renderer'; export const resultComponents = [ @@ -15,5 +16,6 @@ export const resultComponents = [ UrlDisplayComponent, DatatableComponent, DatatableActionsComponent, + DownloadFileTabComponent, rendererComponents ]; diff --git a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts index db6f92c8d0abe1335808f6017f972f99a488ae90..50c551818b7e85b28e6891ef8f60f66c5c1c2310 100644 --- a/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts +++ b/client/src/app/instance/search/components/result/renderer/download-renderer.component.ts @@ -7,8 +7,7 @@ * file that was distributed with this source code. */ -import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; import { DownloadRendererConfig } from 'src/app/metamodel/models/renderers/download-renderer-config.model'; import { getHost } from 'src/app/shared/utils'; @@ -28,8 +27,9 @@ export class DownloadRendererComponent { @Input() datasetName: string; @Input() datasetPublic: boolean; @Input() config: DownloadRendererConfig; + @Output() startsDownloadingFile: EventEmitter<{url: string, filename: string}> = new EventEmitter(); - constructor(private appConfig: AppConfigService, private http: HttpClient) { } + constructor(private appConfig: AppConfigService) { } /** * Returns link href. @@ -50,21 +50,14 @@ export class DownloadRendererComponent { } /** - * Downloads file on click. + * Starts downloading file on click. */ click(event): void { event.preventDefault(); + + const url = this.getHref(); + const filename = url.substring(url.lastIndexOf('/') + 1); - const href = this.getHref(); - this.http.get(href, { responseType: "blob" }).subscribe( - data => { - const filename = href.substring(href.lastIndexOf('/') + 1); - - let downloadLink = document.createElement('a'); - downloadLink.href = window.URL.createObjectURL(data); - downloadLink.setAttribute('download', filename); - downloadLink.click(); - } - ); + this.startsDownloadingFile.emit({ url, filename }) } } diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index 6ab41d2cd824b5b049c07165a48da4f90bdd5428..7775c355e9de9b612db54af4ed69045e9e48bdfc 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -25,6 +25,10 @@ selected with <span class="font-weight-bold">{{ dataLength | async }}</span> objects found. </div> </div> + <app-download-file-tab + *ngIf="(downloadedFiles | async).length > 0" + [downloadedFiles]="downloadedFiles | async"> + </app-download-file-tab> <app-download [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" @@ -33,7 +37,8 @@ [coneSearch]="coneSearch | async" [dataLength]="dataLength | async" [sampRegistered]="sampRegistered | async" - (broadcast)="broadcastVotable($event)"> + (broadcast)="broadcastVotable($event)" + (startsDownloadingFile)="startsDownloadingFile($event)"> </app-download> <app-reminder [datasetSelected]="datasetSelected | async" @@ -77,7 +82,8 @@ (retrieveData)="retrieveData($event)" (addSelectedData)="addSearchData($event)" (deleteSelectedData)="deleteSearchData($event)" - (broadcast)="broadcastVotable($event)"> + (broadcast)="broadcastVotable($event)" + (startsDownloadingFile)="startsDownloadingFile($event)"> </app-datatable-tab> </ng-container> </div> diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts index 599e5cb56a342dc18cb8fae409fc70da7e8a2831..90fc23194beed185c86b27bbd7d51eb3515f4276 100644 --- a/client/src/app/instance/search/containers/result.component.ts +++ b/client/src/app/instance/search/containers/result.component.ts @@ -13,13 +13,15 @@ import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; import { AbstractSearchComponent } from './abstract-search.component'; -import { Pagination } from '../../store/models'; +import { Pagination, DownloadFile } from '../../store/models'; import { Instance } from 'src/app/metamodel/models'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; import * as searchActions from '../../store/actions/search.actions'; import * as searchSelector from '../../store/selectors/search.selector'; import * as sampActions from '../../store/actions/samp.actions'; import * as sampSelector from '../../store/selectors/samp.selector'; +import * as downloadFileActions from '../../store/actions/download-file.actions'; +import * as downloadFileSelector from '../../store/selectors/download-file.selector'; /** * @class @@ -42,6 +44,7 @@ export class ResultComponent extends AbstractSearchComponent { public dataIsLoaded: Observable<boolean>; public selectedData: Observable<any>; public sampRegistered: Observable<boolean>; + public downloadedFiles: Observable<DownloadFile[]>; public pristineSubscription: Subscription; constructor(protected store: Store<{ }>) { @@ -55,6 +58,7 @@ export class ResultComponent extends AbstractSearchComponent { this.dataIsLoaded = this.store.select(searchSelector.selectDataIsLoaded); this.selectedData = this.store.select(searchSelector.selectSelectedData); this.sampRegistered = this.store.select(sampSelector.selectRegistered); + this.downloadedFiles = this.store.select(downloadFileSelector.selectDownloadedFiles); } ngOnInit(): void { @@ -120,6 +124,15 @@ export class ResultComponent extends AbstractSearchComponent { this.store.dispatch(searchActions.deleteSelectedData({ id })); } + /** + * Dispatches action to starts downloading file. + * + * @param {url: string, filename: string} download - Info about file to download + */ + startsDownloadingFile(download: {url: string, filename: string}): void { + this.store.dispatch(downloadFileActions.startsDownloadingFile(download)); + } + /** * Dispatches action to destroy search results. */ diff --git a/client/src/app/instance/store/actions/download-file.actions.ts b/client/src/app/instance/store/actions/download-file.actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5ff418da3290c6dd1108f964d8b6d28bea38b02 --- /dev/null +++ b/client/src/app/instance/store/actions/download-file.actions.ts @@ -0,0 +1,14 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createAction, props } from '@ngrx/store'; + +export const startsDownloadingFile = createAction('[File] Starts Downloading File', props<{ url: string, filename: string }>()); +export const updateDownloadProgress = createAction('[File] Update Download Progress', props<{ progress: number, filename: string }>()); +export const startsArchiveFilesCreation = createAction('[File] Starts Archive Files Creation'); diff --git a/client/src/app/instance/store/effects/detail.effects.ts b/client/src/app/instance/store/effects/detail.effects.ts index 941f318d5ab5563666b0bbbc78334f47de607d15..b1f2aa5f2bfec3d2c09c3aa7055ee309655cafff 100644 --- a/client/src/app/instance/store/effects/detail.effects.ts +++ b/client/src/app/instance/store/effects/detail.effects.ts @@ -10,7 +10,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; -import { Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { of } from 'rxjs'; import { map, tap, mergeMap, catchError } from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; diff --git a/client/src/app/instance/store/effects/download-file.effects.ts b/client/src/app/instance/store/effects/download-file.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..21091c776b82fc7c4b0af03fa8559ffb4970ae7c --- /dev/null +++ b/client/src/app/instance/store/effects/download-file.effects.ts @@ -0,0 +1,59 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpEventType } from '@angular/common/http'; + +import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import { DownloadFileService } from '../services/download-file.service'; +import * as downloadFileActions from '../actions/download-file.actions'; + +/** + * @class + * @classdesc File effects. + */ +@Injectable() +export class DownloadFileEffects { + /** + * Calls actions to retrieve object. + */ + startsDownloadingFile$ = createEffect(() => + this.actions$.pipe( + ofType(downloadFileActions.startsDownloadingFile), + mergeMap((action) => this.downloadFileService.startsDownloadingFile(action.url) + .pipe( + map(event => { + if (event.type === HttpEventType.DownloadProgress) { + this.store.dispatch(downloadFileActions.updateDownloadProgress({ + progress: Math.round((100 * event.loaded) / event.total), + filename: action.filename + })); + } + + if (event.type === HttpEventType.Response) { + this.downloadFileService.saveDownloadedFile(event.body as Blob, action.filename); + } + }) + ) + ) + ), { dispatch: false } + ); + + constructor( + private actions$: Actions, + private downloadFileService: DownloadFileService, + private store: Store<{ }>, + private toastr: ToastrService + ) {} +} diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 69b9a3b26cc0cd09cc16e974cbb4ddc4102571b9..18cb99de4d7126d13194355146ddacf39b3c8ae5 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -4,6 +4,7 @@ import { SearchMultipleEffects } from './search-multiple.effects'; import { ConeSearchEffects } from './cone-search.effects'; import { DetailEffects } from './detail.effects'; import { SvomJsonKwEffects } from './svom-json-kw.effects'; +import { DownloadFileEffects } from './download-file.effects'; export const instanceEffects = [ SampEffects, @@ -11,5 +12,6 @@ export const instanceEffects = [ SearchMultipleEffects, ConeSearchEffects, DetailEffects, - SvomJsonKwEffects + SvomJsonKwEffects, + DownloadFileEffects ]; diff --git a/client/src/app/instance/store/models/download-file.model.ts b/client/src/app/instance/store/models/download-file.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4d08f454cd6832a508d932f152294a0cd050a6e --- /dev/null +++ b/client/src/app/instance/store/models/download-file.model.ts @@ -0,0 +1,14 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export interface DownloadFile { + name: string, + state: 'PENDING' | 'IN_PROGRESS' | 'DONE' + progress: number +} diff --git a/client/src/app/instance/store/models/index.ts b/client/src/app/instance/store/models/index.ts index 2570f1630cacd2de9afd9bda713f92230d6b8f22..3b2241bcfbdd3629726f8766f5220e0b74b9ccf1 100644 --- a/client/src/app/instance/store/models/index.ts +++ b/client/src/app/instance/store/models/index.ts @@ -7,4 +7,5 @@ export * from './cone-search.model'; export * from './resolver.model'; export * from './search-multiple-dataset-length'; export * from './search-multiple-dataset-data'; -export * from './svom-keyword.model'; \ No newline at end of file +export * from './svom-keyword.model'; +export * from './download-file.model'; diff --git a/client/src/app/instance/store/reducers/download-file.reducer.ts b/client/src/app/instance/store/reducers/download-file.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0021b4683c468798cd401a0b49314989448e1fc --- /dev/null +++ b/client/src/app/instance/store/reducers/download-file.reducer.ts @@ -0,0 +1,51 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createReducer, on } from '@ngrx/store'; + +import { DownloadFile } from '../models'; +import * as downloadFileActions from '../actions/download-file.actions'; + +/** + * Interface for file state. + * + * @interface State + */ +export interface State { + downloadedFiles: DownloadFile[] +} + +export const initialState: State = { + downloadedFiles: [] +}; + +export const fileReducer = createReducer( + initialState, + on(downloadFileActions.startsDownloadingFile, (state, { filename }) => ({ + ...state, + downloadedFiles: [...state.downloadedFiles, { + name: filename, + state: 'PENDING', + progress: 0 + }] + })), + on(downloadFileActions.updateDownloadProgress, (state, { progress, filename }) => ({ + ...state, + downloadedFiles: [ + ...state.downloadedFiles.filter(f => f.name !== filename), + { + name: filename, + state: 'IN_PROGRESS', + progress: progress + } + ] + })) +); + +export const selectDownloadedFiles = (state: State) => state.downloadedFiles; \ No newline at end of file diff --git a/client/src/app/instance/store/selectors/download-file.selector.ts b/client/src/app/instance/store/selectors/download-file.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..b955521d86de25bbf5c6eef631a4b77e4f2bd982 --- /dev/null +++ b/client/src/app/instance/store/selectors/download-file.selector.ts @@ -0,0 +1,23 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createSelector } from '@ngrx/store'; + +import * as reducer from '../../instance.reducer'; +import * as fromDownloadFile from '../reducers/download-file.reducer'; + +export const selectDownloadFileState = createSelector( + reducer.getInstanceState, + (state: reducer.State) => state.downloadFile +); + +export const selectDownloadedFiles = createSelector( + selectDownloadFileState, + fromDownloadFile.selectDownloadedFiles +); diff --git a/client/src/app/instance/store/services/download-file.service.ts b/client/src/app/instance/store/services/download-file.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..663e052129cd09d9cc46d0ae09ea4f19fdf2ae89 --- /dev/null +++ b/client/src/app/instance/store/services/download-file.service.ts @@ -0,0 +1,34 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpRequest } from '@angular/common/http'; + +@Injectable({providedIn: 'root'}) +export class DownloadFileService { + constructor(private http: HttpClient) { } + + startsDownloadingFile(url: string) { + const request = new HttpRequest( + "GET", + url, + {}, + { reportProgress: true, responseType: 'blob' } + ); + + return this.http.request(request); + } + + saveDownloadedFile(body: Blob, filename: string) { + let downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(body); + downloadLink.setAttribute('download', filename); + downloadLink.click(); + } +} \ No newline at end of file diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts index 795315e994b19b011ba88241a4428380e6932c09..d70ddbf82870dc1059472f410111eb4979b00052 100644 --- a/client/src/app/instance/store/services/index.ts +++ b/client/src/app/instance/store/services/index.ts @@ -3,11 +3,13 @@ import { SampService } from './samp.service'; import { ConeSearchService } from './cone-search.service'; import { DetailService } from './detail.service'; import { SvomJsonKwService } from './svom-json-kw.service'; +import { DownloadFileService } from './download-file.service'; export const instanceServices = [ SearchService, SampService, ConeSearchService, DetailService, - SvomJsonKwService + SvomJsonKwService, + DownloadFileService ]; diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index bfb8594e449030fea934eab9a1143c8ead325adb..5455dacaf03013492277fba8166c698db0219f76 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -20,6 +20,7 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { PaginationModule } from 'ngx-bootstrap/pagination'; +import { ProgressbarModule } from 'ngx-bootstrap/progressbar'; import { NgSelectModule } from '@ng-select/ng-select'; import { NgxJsonViewerModule } from 'ngx-json-viewer'; @@ -48,6 +49,7 @@ import { sharedPipes } from './pipes'; BsDatepickerModule.forRoot(), TabsModule.forRoot(), PaginationModule.forRoot(), + ProgressbarModule.forRoot(), NgSelectModule, NgxJsonViewerModule ], @@ -63,6 +65,7 @@ import { sharedPipes } from './pipes'; BsDatepickerModule, TabsModule, PaginationModule, + ProgressbarModule, NgSelectModule, NgxJsonViewerModule, sharedComponents,