You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2026-05-16 09:21:12 +02:00
Improving upload UI #1118
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpResponse} from '@angular/common/http';
|
||||
import {HttpClient, HttpEvent, HttpEventType, HttpResponse} from '@angular/common/http';
|
||||
import {Message} from '../../../../common/entities/Message';
|
||||
import {LoadingBarService} from '@ngx-loading-bar/core';
|
||||
import {ErrorCodes, ErrorDTO} from '../../../../common/entities/Error';
|
||||
@@ -7,7 +7,7 @@ import {Config} from '../../../../common/config/public/Config';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {CustomHeaders} from '../../../../common/CustomHeaders';
|
||||
import {VersionService} from '../version.service';
|
||||
import {lastValueFrom} from 'rxjs';
|
||||
import {lastValueFrom, Observable, tap} from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class NetworkService {
|
||||
@@ -80,6 +80,33 @@ export class NetworkService {
|
||||
return this.callJson('post', url, data);
|
||||
}
|
||||
|
||||
public postFormData<T>(url: string, data: FormData): Observable<HttpEvent<Message<T>>> {
|
||||
this.loadingBarService.useRef().start();
|
||||
return this.http.post<Message<T>>(this.apiBaseUrl + url, data, {
|
||||
reportProgress: true,
|
||||
observe: 'events',
|
||||
}).pipe(tap({
|
||||
next: (event) => {
|
||||
if (event.type === HttpEventType.Response) {
|
||||
this.loadingBarService.useRef().complete();
|
||||
if (event.headers.has(CustomHeaders.dataVersion)) {
|
||||
this.versionService.onNewVersion(
|
||||
event.headers.get(CustomHeaders.dataVersion)
|
||||
);
|
||||
}
|
||||
if (event.headers.has(CustomHeaders.appVersion)) {
|
||||
this.versionService.onNewAppVersion(
|
||||
event.headers.get(CustomHeaders.appVersion)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loadingBarService.useRef().complete();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public putJson<T>(url: string, data = {}): Promise<T> {
|
||||
return this.callJson('put', url, data);
|
||||
}
|
||||
|
||||
@@ -5,45 +5,123 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(var(--bs-body-bg-rgb), 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.upload-overlay.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.upload-overlay-content {
|
||||
background: var(--bs-body-bg);
|
||||
padding: 2em;
|
||||
padding: 3em;
|
||||
text-align: center;
|
||||
border: 2px dashed var(--bs-primary);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.upload-overlay.active .upload-overlay-content {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.upload-overlay-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--bs-primary);
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {transform: translateY(0);}
|
||||
40% {transform: translateY(-20px);}
|
||||
60% {transform: translateY(-10px);}
|
||||
}
|
||||
|
||||
.upload-overlay-text {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--bs-emphasis-color);
|
||||
}
|
||||
|
||||
.upload-progress-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 350px;
|
||||
width: 380px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
z-index: 10001;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.upload-progress-container > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.upload-progress-list {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.upload-progress-item {
|
||||
transition: opacity 0.5s ease-out;
|
||||
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.upload-progress-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.upload-progress-item.fading {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
cursor: pointer;
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.summary-card .progress {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.summary-card .progress-bar {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.progress {
|
||||
border-radius: 10px;
|
||||
background-color: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="upload-overlay" *ngIf="isUploadOver">
|
||||
<div class="upload-overlay-content rounded">
|
||||
<div class="upload-overlay" [class.active]="isUploadOver" *ngIf="isUploadOver">
|
||||
<div class="upload-overlay-content">
|
||||
<div class="upload-overlay-icon">
|
||||
<ng-icon name="ionCloudUploadOutline" size="4em"></ng-icon>
|
||||
</div>
|
||||
@@ -7,57 +7,65 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-progress-container" *ngIf="uploaderService.uploadProgress.length > 0">
|
||||
<!-- Show summary if many files are uploading -->
|
||||
<ng-container *ngIf="uploaderService.uploadProgress.length > 5 && !showDetails; else detailedList">
|
||||
<div class="card mb-2 upload-progress-item summary-card" (click)="toggleDetails()">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-truncate me-2" i18n>Uploading {{ uploaderService.uploadProgress.length }} files...</small>
|
||||
<ng-icon name="ionChevronUpOutline"></ng-icon>
|
||||
</div>
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
|
||||
[ngStyle]="{'width': getOverallProgress() + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #detailedList>
|
||||
<div *ngIf="uploaderService.uploadProgress.length > 5 && showDetails"
|
||||
class="card mb-2 upload-progress-item summary-card" (click)="toggleDetails()">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-truncate me-2" i18n>Hide details ({{ uploaderService.uploadProgress.length }} files)</small>
|
||||
<ng-icon name="ionChevronDownOutline"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let item of uploaderService.uploadProgress">
|
||||
<div class="card mb-2 upload-progress-item"
|
||||
[class.fading]="item.done && item.lastUpdate < Date.now() - 4500"
|
||||
*ngIf="item.lastUpdate > Date.now() - 5000 || !item.done">
|
||||
<div class="upload-progress-list">
|
||||
<!-- Show summary if many files are uploading -->
|
||||
<ng-container *ngIf="uploaderService.uploadProgress.length > 5 && !showDetails; else detailedList">
|
||||
<div class="card mb-2 upload-progress-item summary-card" (click)="toggleDetails()">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-truncate me-2" [title]="item.name">{{ item.name }}</small>
|
||||
<small *ngIf="!item.error && item.status !== 'queued'">{{ item.progress }}%</small>
|
||||
<small *ngIf="item.status === 'queued'" i18n>Queued</small>
|
||||
<small *ngIf="item.error" class="text-danger" [title]="item.error">
|
||||
<ng-container *ngIf="item.count" i18n>Already exists</ng-container>
|
||||
<ng-container *ngIf="!item.count" i18n>Error</ng-container>
|
||||
</small>
|
||||
<small class="text-truncate me-2 fw-bold" i18n>Uploading {{ uploaderService.uploadProgress.length }} files...</small>
|
||||
<ng-icon name="ionChevronUpOutline"></ng-icon>
|
||||
</div>
|
||||
<div *ngIf="item.error && !item.count" class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-danger">{{item.error}}</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 5px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
[class.bg-danger]="item.error"
|
||||
[class.bg-success]="item.done && !item.error"
|
||||
[ngStyle]="{'width': item.progress + '%'}"></div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
|
||||
[ngStyle]="{'width': getOverallProgress() + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #detailedList>
|
||||
<div *ngIf="uploaderService.uploadProgress.length > 5 && showDetails"
|
||||
class="card mb-2 upload-progress-item summary-card" (click)="toggleDetails()">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-truncate me-2 fw-bold" i18n>Hide details ({{ uploaderService.uploadProgress.length }} files)</small>
|
||||
<ng-icon name="ionChevronDownOutline"></ng-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngFor="let item of uploaderService.uploadProgress">
|
||||
<div class="card mb-2 upload-progress-item"
|
||||
[class.fading]="item.done && item.lastUpdate < Date.now() - 4500"
|
||||
*ngIf="item.lastUpdate > Date.now() - 5000 || !item.done">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<small class="text-truncate me-2" [title]="item.name" style="max-width: 70%;">{{ item.name }}</small>
|
||||
<div class="d-flex align-items-center">
|
||||
<small class="me-1" *ngIf="!item.error && item.status !== 'queued'">{{ item.progress }}%</small>
|
||||
<small *ngIf="item.status === 'queued'" class="text-muted" i18n>Queued</small>
|
||||
<ng-icon *ngIf="item.done && !item.error" name="ionCheckmarkCircle" class="text-success"></ng-icon>
|
||||
<ng-icon *ngIf="item.error" name="ionAlertCircle" class="text-danger"></ng-icon>
|
||||
<small *ngIf="item.error" class="text-danger ms-1" [title]="item.error">
|
||||
<ng-container *ngIf="item.count" i18n>Already exists</ng-container>
|
||||
<ng-container *ngIf="!item.count" i18n>Error</ng-container>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="item.error && !item.count" class="mb-2">
|
||||
<small class="text-danger d-block text-truncate" [title]="item.error">{{item.error}}</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
[class.bg-danger]="item.error"
|
||||
[class.bg-success]="item.done && !item.error"
|
||||
[class.progress-bar-animated]="item.status === 'uploading'"
|
||||
[class.progress-bar-striped]="item.status === 'uploading'"
|
||||
[ngStyle]="{'width': item.progress + '%'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpEventType} from '@angular/common/http';
|
||||
import {HttpEventType} from '@angular/common/http';
|
||||
import {NetworkService} from '../../../model/network/network.service';
|
||||
import {SupportedFormats} from '../../../../../common/SupportedFormats';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
@@ -39,8 +39,7 @@ export class UploaderService {
|
||||
private lastProgressSum = 0;
|
||||
private lastUpdateTimestamp = 0;
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private networkService: NetworkService,
|
||||
constructor(private networkService: NetworkService,
|
||||
private notificationService: NotificationService,
|
||||
private authService: AuthenticationService,
|
||||
private contentLoaderService: ContentLoaderService) {
|
||||
@@ -199,12 +198,9 @@ export class UploaderService {
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
|
||||
const url = Utils.concatUrls(this.networkService.apiBaseUrl, '/upload/', directory || '');
|
||||
const url = '/upload/' + (directory || '');
|
||||
|
||||
this.http.post(url, formData, {
|
||||
reportProgress: true,
|
||||
observe: 'events'
|
||||
}).subscribe({
|
||||
this.networkService.postFormData<any>(url, formData).subscribe({
|
||||
next: (event) => {
|
||||
if (event.type === HttpEventType.UploadProgress) {
|
||||
progressItem.progress = Math.round((100 * event.loaded) / event.total);
|
||||
|
||||
Reference in New Issue
Block a user