mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: use 'share sheet' for download buttons on mobile (#8949)
This commit is contained in:
@@ -25,15 +25,20 @@
|
||||
</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</a>
|
||||
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading">
|
||||
@if (downloading) {
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
} @else {
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs>
|
||||
}
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</button>
|
||||
|
||||
@if (metadata?.has_archive_version) {
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" [disabled]="downloading" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
<button ngbDropdownItem (click)="download(true)" [disabled]="downloading" i18n>Download original</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import {
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
@@ -127,6 +128,7 @@ describe('DocumentDetailComponent', () => {
|
||||
let documentListViewService: DocumentListViewService
|
||||
let settingsService: SettingsService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let deviceDetectorService: DeviceDetectorService
|
||||
let httpTestingController: HttpTestingController
|
||||
let componentRouterService: ComponentRouterService
|
||||
|
||||
@@ -264,6 +266,7 @@ describe('DocumentDetailComponent', () => {
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 1 }
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
deviceDetectorService = TestBed.inject(DeviceDetectorService)
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
componentRouterService = TestBed.inject(ComponentRouterService)
|
||||
@@ -1268,4 +1271,38 @@ describe('DocumentDetailComponent', () => {
|
||||
.error(new ErrorEvent('failed'))
|
||||
expect(component.tiffError).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support download using share sheet on mobile, direct download otherwise', () => {
|
||||
const shareSpy = jest.spyOn(navigator, 'share')
|
||||
const createSpy = jest.spyOn(document, 'createElement')
|
||||
const urlRevokeSpy = jest.spyOn(URL, 'revokeObjectURL')
|
||||
initNormally()
|
||||
|
||||
// Mobile
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(false)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.error(new ProgressEvent('failed'))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
|
||||
component.download(true)
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}documents/${doc.id}/download/?original=true`
|
||||
)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).toHaveBeenCalled()
|
||||
|
||||
// Desktop
|
||||
shareSpy.mockClear()
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(true)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
expect(createSpy).toHaveBeenCalledWith('a')
|
||||
expect(urlRevokeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@@ -20,6 +20,7 @@ import {
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
@@ -195,8 +196,6 @@ export class DocumentDetailComponent
|
||||
previewUrl: string
|
||||
thumbUrl: string
|
||||
previewText: string
|
||||
downloadUrl: string
|
||||
downloadOriginalUrl: string
|
||||
previewLoaded: boolean = false
|
||||
tiffURL: string
|
||||
tiffError: string
|
||||
@@ -234,6 +233,9 @@ export class DocumentDetailComponent
|
||||
ogDate: Date
|
||||
|
||||
customFields: CustomField[]
|
||||
|
||||
public downloading: boolean = false
|
||||
|
||||
public readonly CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
public readonly ContentRenderType = ContentRenderType
|
||||
@@ -274,7 +276,8 @@ export class DocumentDetailComponent
|
||||
private customFieldsService: CustomFieldsService,
|
||||
private http: HttpClient,
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService
|
||||
private componentRouterService: ComponentRouterService,
|
||||
private deviceDetectorService: DeviceDetectorService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -417,13 +420,6 @@ export class DocumentDetailComponent
|
||||
.pipe(
|
||||
switchMap((doc) => {
|
||||
this.documentId = doc.id
|
||||
this.downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId
|
||||
)
|
||||
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
true
|
||||
)
|
||||
this.suggestions = null
|
||||
const openDocument = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
@@ -978,6 +974,52 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
download(original: boolean = false) {
|
||||
this.downloading = true
|
||||
const downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
original
|
||||
)
|
||||
this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloading = false
|
||||
const blobParts = [blob]
|
||||
const file = new File(
|
||||
blobParts,
|
||||
original
|
||||
? this.document.original_file_name
|
||||
: this.document.archived_file_name,
|
||||
{
|
||||
type: original ? this.document.mime_type : 'application/pdf',
|
||||
}
|
||||
)
|
||||
if (
|
||||
!this.deviceDetectorService.isDesktop() &&
|
||||
navigator.canShare &&
|
||||
navigator.canShare({ files: [file] })
|
||||
) {
|
||||
navigator.share({
|
||||
files: [file],
|
||||
})
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = this.document.title
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.downloading = false
|
||||
this.toastService.showError(
|
||||
$localize`Error downloading document`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return this.documentListViewService.hasNext(this.documentId)
|
||||
}
|
||||
|
Reference in New Issue
Block a user