Compare commits
1 Commits
898d217451
...
9887903f49
| Author | SHA1 | Date | |
|---|---|---|---|
| 9887903f49 |
@@ -5,7 +5,7 @@ import classNames from 'classnames';
|
||||
import { Box, Button, Chip, Header, IconButton, Input, Menu, PopOut, RectCords, Scroll, Spinner, Text, as, config } from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import FileSaver from 'file-saver';
|
||||
import { saveMediaBlob } from '../../utils/saveMedia';
|
||||
import * as css from './PdfViewer.css';
|
||||
import { AsyncStatus } from '../../hooks/useAsyncCallback';
|
||||
import { useZoom } from '../../hooks/useZoom';
|
||||
@@ -61,8 +61,14 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||
}
|
||||
}, [docState, pageNo, zoom]);
|
||||
|
||||
const handleDownload = () => {
|
||||
FileSaver.saveAs(src, name);
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error('Failed to fetch PDF');
|
||||
await saveMediaBlob(await res.blob(), name);
|
||||
} catch (error) {
|
||||
console.warn('[PdfViewer] Failed to download:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJumpSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||
|
||||
@@ -6,12 +6,11 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import classNames from 'classnames';
|
||||
import { as } from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import * as css from './ImageViewer.css';
|
||||
import { downloadMedia } from '../../utils/matrix';
|
||||
import { downloadAndSaveMedia } from '../../utils/saveMedia';
|
||||
import { getCurrentAccessToken } from '../../utils/auth';
|
||||
|
||||
const ZOOM_MIN = 1;
|
||||
@@ -270,18 +269,13 @@ export const ImageViewer = as<'div', ImageViewerProps>(
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const fileContent = await downloadMedia(src, getCurrentAccessToken());
|
||||
FileSaver.saveAs(fileContent, alt);
|
||||
await downloadAndSaveMedia(src, alt, getCurrentAccessToken());
|
||||
} catch (error) {
|
||||
console.warn('[ImageViewer] Failed to download media:', error);
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
FileSaver.saveAs(blob, alt);
|
||||
}
|
||||
} catch {
|
||||
window.open(src, '_blank');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Badge, Box, IconButton, Spinner, Text, as, toRem } from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||
import FileSaver from 'file-saver';
|
||||
import { mimeTypeToExt } from '../../utils/mimeTypes';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
mxcUrlToHttp,
|
||||
} from '../../utils/matrix';
|
||||
import { getCurrentAccessToken } from '../../utils/auth';
|
||||
import { saveMediaBlob } from '../../utils/saveMedia';
|
||||
|
||||
const badgeStyles = { maxWidth: toRem(100) };
|
||||
|
||||
@@ -36,9 +36,8 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
|
||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo), accessToken)
|
||||
: await downloadMedia(mediaUrl, accessToken);
|
||||
|
||||
const fileURL = URL.createObjectURL(fileContent);
|
||||
FileSaver.saveAs(fileURL, filename);
|
||||
return fileURL;
|
||||
await saveMediaBlob(fileContent, filename);
|
||||
return true;
|
||||
}, [mx, url, useAuthentication, mimeType, encInfo, filename])
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { Box, Button, Modal, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, Tooltip, TooltipProvider, as } from 'folds';
|
||||
import { Icon, Icons } from '../../icons';
|
||||
import FileSaver from 'file-saver';
|
||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||
import { saveMediaBlob } from '../../../utils/saveMedia';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { IFileInfo } from '../../../../types/matrix/common';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
|
||||
@@ -252,9 +252,8 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
|
||||
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo), accessToken)
|
||||
: await downloadMedia(mediaUrl, accessToken);
|
||||
|
||||
const fileURL = URL.createObjectURL(fileContent);
|
||||
FileSaver.saveAs(fileURL, body);
|
||||
return fileURL;
|
||||
await saveMediaBlob(fileContent, body);
|
||||
return fileContent;
|
||||
}, [mx, url, useAuthentication, mimeType, encInfo, body])
|
||||
);
|
||||
|
||||
@@ -268,7 +267,7 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
|
||||
size="400"
|
||||
onClick={() =>
|
||||
downloadState.status === AsyncStatus.Success
|
||||
? FileSaver.saveAs(downloadState.data, body)
|
||||
? saveMediaBlob(downloadState.data, body)
|
||||
: download()
|
||||
}
|
||||
disabled={downloadState.status === AsyncStatus.Loading}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import classNames from 'classnames';
|
||||
import { Box, Chip, Header, IconButton, Text, as } from 'folds';
|
||||
import { Icon, Icons } from '../icons';
|
||||
import * as css from './VideoViewer.css';
|
||||
import { downloadMedia } from '../../utils/matrix';
|
||||
import { downloadAndSaveMedia } from '../../utils/saveMedia';
|
||||
import { getCurrentAccessToken } from '../../utils/auth';
|
||||
|
||||
export type VideoViewerProps = {
|
||||
@@ -14,20 +13,16 @@ export type VideoViewerProps = {
|
||||
};
|
||||
|
||||
export const VideoViewer = as<'div', VideoViewerProps>(
|
||||
({ className, alt, src, requestClose, ...props }, ref) => { const handleDownload = async () => {
|
||||
({ className, alt, src, requestClose, ...props }, ref) => {
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const fileContent = await downloadMedia(src, getCurrentAccessToken());
|
||||
FileSaver.saveAs(fileContent, alt);
|
||||
await downloadAndSaveMedia(src, alt, getCurrentAccessToken());
|
||||
} catch (error) {
|
||||
console.warn('[VideoViewer] Failed to download media:', error);
|
||||
try {
|
||||
const response = await fetch(src);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
FileSaver.saveAs(blob, alt);
|
||||
}
|
||||
} catch {
|
||||
window.open(src, '_blank');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
55
src/app/utils/saveMedia.ts
Normal file
55
src/app/utils/saveMedia.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import FileSaver from 'file-saver';
|
||||
import { downloadMedia } from './matrix';
|
||||
import { getCurrentAccessToken } from './auth';
|
||||
|
||||
export type MediaSaver = (blob: Blob, filename: string) => void | Promise<void>;
|
||||
|
||||
let customMediaSaver: MediaSaver | null = null;
|
||||
|
||||
const defaultMediaSaver: MediaSaver = (blob, filename) => {
|
||||
FileSaver.saveAs(blob, filename);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a platform-specific media saver (Android MediaStore, Electron dialog, etc.).
|
||||
* Pass null to restore the default FileSaver path.
|
||||
*
|
||||
* Platforms should call this once at startup (mobile overlay / desktop shell entry).
|
||||
*/
|
||||
export const registerMediaSaver = (saver: MediaSaver | null): void => {
|
||||
customMediaSaver = saver;
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist a Blob to disk via the registered platform saver, or FileSaver by default.
|
||||
*/
|
||||
export const saveMediaBlob = async (blob: Blob, filename: string): Promise<void> => {
|
||||
const saver = customMediaSaver ?? defaultMediaSaver;
|
||||
await saver(blob, filename);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch media (auth download or blob:/data:/http URL) and save it via [saveMediaBlob].
|
||||
*/
|
||||
export const downloadAndSaveMedia = async (
|
||||
src: string,
|
||||
filename: string,
|
||||
accessToken?: string | null
|
||||
): Promise<void> => {
|
||||
let blob: Blob;
|
||||
try {
|
||||
if (src.startsWith('blob:') || src.startsWith('data:')) {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${src}`);
|
||||
blob = await res.blob();
|
||||
} else {
|
||||
blob = await downloadMedia(src, accessToken ?? getCurrentAccessToken());
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[saveMedia] downloadMedia failed, trying fetch fallback:', error);
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw error;
|
||||
blob = await res.blob();
|
||||
}
|
||||
await saveMediaBlob(blob, filename);
|
||||
};
|
||||
Reference in New Issue
Block a user