Feature: use 'share sheet' for download buttons on mobile (#8949)

This commit is contained in:
shamoon
2025-01-30 08:31:52 -08:00
committed by GitHub
parent 427508edf1
commit f56974f158
7 changed files with 245 additions and 130 deletions

View File

@@ -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>
}

View File

@@ -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()
})
})

View File

@@ -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)
}