From 39a8948976d7d9e113c8578d4298c5926af4ddac Mon Sep 17 00:00:00 2001 From: Helmut Date: Thu, 28 May 2026 07:00:40 +0200 Subject: [PATCH 1/4] feat(ui): add iOS Web Share button next to download link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ui/src/app/app.html | 3 +++ ui/src/app/app.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index b2ae0db..26f496a 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -854,6 +854,9 @@ @if (entry[1].filename) { } + @if (entry[1].filename && canShareDownloads()) { + + } diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index a73f879..03448ee 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -7,7 +7,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 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 { CookieService } from 'ngx-cookie-service'; import { AddDownloadPayload, DownloadsService } from './services/downloads.service'; @@ -185,6 +185,7 @@ export class App implements AfterViewInit, OnInit, OnDestroy { faUpload = faUpload; faPause = faPause; faPlay = faPlay; + faShareNodes = faShareNodes; subtitleLanguages = [ { id: 'en', text: 'English' }, { id: 'ar', text: 'Arabic' }, @@ -1189,6 +1190,51 @@ export class App implements AfterViewInit, OnInit, OnDestroy { 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 { + 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) { const parts = []; if (download.msg) { From 6ff364aacf8e993adf0ec56d4cec6ad508a09ab0 Mon Sep 17 00:00:00 2001 From: Helmut Date: Fri, 29 May 2026 04:56:52 +0200 Subject: [PATCH 2/4] feat(ui): warn before share + surface failures for large files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web Share fails silently when iOS' share sheet refuses the payload, typically because the file exceeds the platform's soft size limit (~50–100 MB depending on iOS version). The previous patch logged to the console but the user saw nothing — staring at a button that 'does nothing' is poor UX. Adds two layers of feedback: 1. Pre-flight size check (SHARE_SIZE_WARN_BYTES = 80 MB, conservative relative to iOS' actual limit) with a confirm() dialog before the fetch. Avoids spending bandwidth pulling a 150 MB blob into the browser only for navigator.canShare to reject it. 2. Surfaces canShare-rejection AND share()-failure as a visible alert() suggesting the user fall back to the download link next to the share button. Tested locally with files from 0.7 MB up to 150.7 MB: small files share unchanged, the 150 MB file now produces a pre-flight warning the user can dismiss, and any subsequent rejection produces a clear alert instead of silently no-op'ing. --- ui/src/app/app.ts | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 03448ee..089b78f 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1203,10 +1203,28 @@ export class App implements AfterViewInit, OnInit, OnDestroy { && typeof navigator.canShare === 'function'; } + // Conservative warning threshold for the share sheet — iOS' actual + // refusal limit varies between ~50 MB (older versions) and ~150 MB + // (recent ones). 80 MB warns the user before the time-wasting fetch+ + // copy of a too-large file that the platform will then reject. + private static readonly SHARE_SIZE_WARN_BYTES = 80 * 1024 * 1024; + async shareDownload(download: Download): Promise { if (!this.canShareDownloads()) { return; } + // Pre-flight size check: warn the user about the iOS share-sheet + // soft-fail on large files, before we spend time fetching the whole + // file into memory only to have navigator.canShare reject it. + if (download.size && download.size > App.SHARE_SIZE_WARN_BYTES) { + const sizeMb = Math.round(download.size / 1024 / 1024); + const proceed = window.confirm( + `This file is ${sizeMb} MB. iOS' share sheet often refuses files ` + + `larger than ~100 MB and the share will silently fail. ` + + `Try anyway? (Use the download button instead if it fails.)` + ); + if (!proceed) return; + } try { const response = await fetch(this.buildDownloadLink(download)); if (!response.ok) { @@ -1218,20 +1236,28 @@ export class App implements AfterViewInit, OnInit, OnDestroy { }); 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. + // The platform refused the payload — most commonly because the + // file is too large for the iOS share sheet, or the MIME type + // isn't accepted. Tell the user so they can fall back to the + // download button right next to this one instead of staring at + // a button that quietly did nothing. console.warn('navigator.canShare rejected payload for', download.filename); + window.alert( + `Your device's share sheet doesn't accept this file ` + + `(most likely because it's too large). ` + + `Please use the download button instead.` + ); 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); - } + if (err?.name === 'AbortError') return; + console.error('Share failed:', err); + window.alert( + `Share failed: ${err?.message || 'unknown error'}. ` + + `Please use the download button instead.` + ); } } From ad92607a21bc58fe4ca985f2382e293f609da0a4 Mon Sep 17 00:00:00 2001 From: Helmut Date: Fri, 29 May 2026 05:24:49 +0200 Subject: [PATCH 3/4] fix(ui): drop redundant tooltip on share button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS doesn't have hover, so the tooltip only ever showed on desktop — where the share-arrow glyph is universally recognised anyway. Aria- label stays for screen readers. --- ui/src/app/app.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 26f496a..847a628 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -855,7 +855,7 @@ } @if (entry[1].filename && canShareDownloads()) { - + } From 56c0ad3b5fdc3a871622c17daf49afb835f53e32 Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Fri, 29 May 2026 13:23:25 +0300 Subject: [PATCH 4/4] fix catch --- ui/src/app/app.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 089b78f..d539070 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1250,12 +1250,13 @@ export class App implements AfterViewInit, OnInit, OnDestroy { return; } await navigator.share(payload); - } catch (err: any) { + } catch (err) { + const e = err as { name?: string; message?: string }; // AbortError = user dismissed the share sheet → silent no-op. - if (err?.name === 'AbortError') return; + if (e.name === 'AbortError') return; console.error('Share failed:', err); window.alert( - `Share failed: ${err?.message || 'unknown error'}. ` + + `Share failed: ${e.message || 'unknown error'}. ` + `Please use the download button instead.` ); }