|
54 | 54 | } |
55 | 55 |
|
56 | 56 | async function triggerDownload(id: string) { |
57 | | - // On mobile, open the native share sheet so users can "Save to Photos". |
58 | | - // Check canShare *synchronously* up front (inside the tap gesture) — this |
59 | | - // matches the original working implementation. Then fetch the file and |
60 | | - // share it; the button stays mounted throughout so iOS keeps the share |
61 | | - // activation alive across the single fetch. |
62 | | - const canShareFiles = |
63 | | - isMobileDevice() && navigator.canShare?.({ files: [new File([], 'test')] }); |
64 | | - if (!canShareFiles) { |
| 57 | + // Desktop: open/download in a new tab (existing behavior). |
| 58 | + if (!isMobileDevice()) { |
65 | 59 | window.open(fileUrl(id), '_blank'); |
66 | 60 | return; |
67 | 61 | } |
| 62 | +
|
| 63 | + // Mobile: fetch the real file, then open the native share sheet so users can |
| 64 | + // "Save to Photos". canShare must be tested against the *real* file with a |
| 65 | + // real MIME type — an empty/type-less probe returns false on iOS WebKit, |
| 66 | + // which is why the old code silently fell through to a blocked window.open. |
| 67 | + // Failures surface as toasts so they're visible on the phone. |
68 | 68 | try { |
69 | 69 | const res = await fetch(fileUrl(id)); |
70 | | - if (!res.ok) throw new Error('fetch failed'); |
| 70 | + if (!res.ok) throw new Error(`HTTP ${res.status}`); |
71 | 71 | const blob = await res.blob(); |
72 | | - const name = filenameFromResponse(res) || 'download'; |
73 | | - const file = new File([blob], name, { type: blob.type }); |
74 | | - await navigator.share({ files: [file] }); |
| 72 | + const name = filenameFromResponse(res) || 'video.mp4'; |
| 73 | + const file = new File([blob], name, { type: blob.type || 'video/mp4' }); |
| 74 | +
|
| 75 | + if (navigator.share && navigator.canShare?.({ files: [file] })) { |
| 76 | + await navigator.share({ files: [file] }); |
| 77 | + return; |
| 78 | + } |
| 79 | +
|
| 80 | + // Share unsupported (e.g. Firefox/iOS) → fall back to a real download. |
| 81 | + addToast('error', 'Share sheet unavailable on this browser — downloading instead'); |
| 82 | + const objUrl = URL.createObjectURL(blob); |
| 83 | + const a = document.createElement('a'); |
| 84 | + a.href = objUrl; |
| 85 | + a.download = name; |
| 86 | + document.body.appendChild(a); |
| 87 | + a.click(); |
| 88 | + a.remove(); |
| 89 | + setTimeout(() => URL.revokeObjectURL(objUrl), 10_000); |
75 | 90 | } catch (e: any) { |
76 | | - if (e.name !== 'AbortError') console.warn('Share failed:', e); |
| 91 | + if (e.name === 'AbortError') return; // user dismissed the share sheet |
| 92 | + addToast('error', `Couldn't share: ${e.name || e.message || e}`); |
77 | 93 | } |
78 | 94 | } |
79 | 95 |
|
|
0 commit comments