mirror of
https://github.com/alexta69/metube.git
synced 2026-06-13 16:40:05 +00:00
feat(ui): add iOS Web Share button next to download link
Adds a share button to the completed-list action row that hands the downloaded file off to the platform share sheet via navigator.share(). On iOS Safari/Chrome this surfaces the native Save-to-Photos / Save-to- Files / AirDrop options for videos and images, and Files / 3rd-party app targets for audio. On platforms without Web Share support (Desktop Firefox/Chrome/Safari) the button hides itself; the existing download link remains the universal fallback. Implementation notes: - canShareDownloads() requires both navigator.share AND navigator.canShare (Desktop Safari has the former without the latter; we always intend to share a file, not a URL) - shareDownload() fetches the file via the existing buildDownloadLink() helper, wraps it in a File, then runs canShare() before share() so we can bail out cleanly on platforms that reject the MIME type - AbortError (user dismisses sheet) is silenced; other errors logged - Tooltip on the button explains the iOS behaviour briefly Refs alexta69/metube#582 — addresses the 'add to Photos.app' request without depending on the iOS Shortcut, which has had reliability issues (cf #763).
This commit is contained in:
@@ -854,6 +854,9 @@
|
|||||||
@if (entry[1].filename) {
|
@if (entry[1].filename) {
|
||||||
<a href="{{buildDownloadLink(entry[1])}}" download class="btn btn-link" [attr.aria-label]="'Download result file for ' + entry[1].title"><fa-icon [icon]="faDownload" /></a>
|
<a href="{{buildDownloadLink(entry[1])}}" download class="btn btn-link" [attr.aria-label]="'Download result file for ' + entry[1].title"><fa-icon [icon]="faDownload" /></a>
|
||||||
}
|
}
|
||||||
|
@if (entry[1].filename && canShareDownloads()) {
|
||||||
|
<button type="button" class="btn btn-link" [attr.aria-label]="'Share result file for ' + entry[1].title" (click)="shareDownload(entry[1])" ngbTooltip="Share — on iOS opens the share sheet (Save to Photos, Files, AirDrop…)"><fa-icon [icon]="faShareNodes" /></button>
|
||||||
|
}
|
||||||
<a href="{{entry[1].url}}" target="_blank" class="btn btn-link" [attr.aria-label]="'Open source URL for ' + entry[1].title"><fa-icon [icon]="faExternalLinkAlt" /></a>
|
<a href="{{entry[1].url}}" target="_blank" class="btn btn-link" [attr.aria-label]="'Open source URL for ' + entry[1].title"><fa-icon [icon]="faExternalLinkAlt" /></a>
|
||||||
<button type="button" class="btn btn-link" [attr.aria-label]="'Delete completed item ' + entry[1].title" (click)="delDownload('done', entry[0])"><fa-icon [icon]="faTrashAlt" /></button>
|
<button type="button" class="btn btn-link" [attr.aria-label]="'Delete completed item ' + entry[1].title" (click)="delDownload('done', entry[0])"><fa-icon [icon]="faTrashAlt" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+47
-1
@@ -7,7 +7,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NgSelectModule } from '@ng-select/ng-select';
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay } from '@fortawesome/free-solid-svg-icons';
|
import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload, faPause, faPlay, faShareNodes } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { CookieService } from 'ngx-cookie-service';
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
import { AddDownloadPayload, DownloadsService } from './services/downloads.service';
|
import { AddDownloadPayload, DownloadsService } from './services/downloads.service';
|
||||||
@@ -185,6 +185,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
|||||||
faUpload = faUpload;
|
faUpload = faUpload;
|
||||||
faPause = faPause;
|
faPause = faPause;
|
||||||
faPlay = faPlay;
|
faPlay = faPlay;
|
||||||
|
faShareNodes = faShareNodes;
|
||||||
subtitleLanguages = [
|
subtitleLanguages = [
|
||||||
{ id: 'en', text: 'English' },
|
{ id: 'en', text: 'English' },
|
||||||
{ id: 'ar', text: 'Arabic' },
|
{ id: 'ar', text: 'Arabic' },
|
||||||
@@ -1189,6 +1190,51 @@ export class App implements AfterViewInit, OnInit, OnDestroy {
|
|||||||
return baseDir + encodeURIComponent(download.filename);
|
return baseDir + encodeURIComponent(download.filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web Share API support — primarily for iOS Safari / Chrome, lets the user
|
||||||
|
// hand the downloaded file off to the platform share sheet (Photos.app,
|
||||||
|
// Files, third-party apps, AirDrop). Falls back silently to the standard
|
||||||
|
// download flow on platforms without navigator.share / canShare.
|
||||||
|
canShareDownloads(): boolean {
|
||||||
|
// navigator.share alone is not enough — Desktop Safari implements
|
||||||
|
// navigator.share but not canShare with files. We explicitly require
|
||||||
|
// both, since we always intend to share a file (not a URL).
|
||||||
|
return typeof navigator !== 'undefined'
|
||||||
|
&& typeof navigator.share === 'function'
|
||||||
|
&& typeof navigator.canShare === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareDownload(download: Download): Promise<void> {
|
||||||
|
if (!this.canShareDownloads()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.buildDownloadLink(download));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status} fetching file for share`);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
const file = new File([blob], download.filename, {
|
||||||
|
type: blob.type || 'application/octet-stream',
|
||||||
|
});
|
||||||
|
const payload: ShareData = { files: [file], title: download.title };
|
||||||
|
if (!navigator.canShare(payload)) {
|
||||||
|
// File type not shareable on this platform (e.g. desktop browsers,
|
||||||
|
// or some MIME types iOS refuses). Bail out so the user can still
|
||||||
|
// use the regular download button right next to this one.
|
||||||
|
console.warn('navigator.canShare rejected payload for', download.filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await navigator.share(payload);
|
||||||
|
} catch (err: any) {
|
||||||
|
// AbortError = user dismissed the share sheet → silent no-op.
|
||||||
|
// Other errors (network, file too big, …) get logged but we don't
|
||||||
|
// surface a UI error: the regular download link remains a fallback.
|
||||||
|
if (err?.name !== 'AbortError') {
|
||||||
|
console.error('Share failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildResultItemTooltip(download: Download) {
|
buildResultItemTooltip(download: Download) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (download.msg) {
|
if (download.msg) {
|
||||||
|
|||||||
Reference in New Issue
Block a user