All checks were successful
Trigger cinny-mobile / dispatch (push) Successful in 1s
Replace legacy profile style metadata with MSC4133 profile fields, add in-app update notes, tighten account profile name spacing, and wire desktop media save helpers through the client.
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
import { registerMediaSaver } from './saveMedia';
|
|
import { isElectron, isTauri } from './tauri';
|
|
|
|
type ElectronSaveResult = {
|
|
success?: boolean;
|
|
canceled?: boolean;
|
|
path?: string;
|
|
error?: string;
|
|
};
|
|
|
|
type ElectronMediaApi = {
|
|
saveFile?: (payload: {
|
|
filename: string;
|
|
mimeType?: string;
|
|
data: Uint8Array;
|
|
}) => Promise<ElectronSaveResult>;
|
|
};
|
|
|
|
async function blobToUint8Array(blob: Blob): Promise<Uint8Array> {
|
|
const buffer = await blob.arrayBuffer();
|
|
return new Uint8Array(buffer);
|
|
}
|
|
|
|
function guessFilters(filename: string, mimeType?: string): Array<{ name: string; extensions: string[] }> {
|
|
const ext = filename.includes('.') ? filename.split('.').pop()!.toLowerCase() : '';
|
|
const mime = (mimeType || '').toLowerCase();
|
|
|
|
if (mime.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) {
|
|
return [
|
|
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
];
|
|
}
|
|
if (mime.startsWith('video/') || ['mp4', 'webm', 'mkv', 'mov'].includes(ext)) {
|
|
return [
|
|
{ name: 'Videos', extensions: ['mp4', 'webm', 'mkv', 'mov'] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
];
|
|
}
|
|
if (mime.startsWith('audio/') || ['mp3', 'ogg', 'wav', 'm4a', 'flac'].includes(ext)) {
|
|
return [
|
|
{ name: 'Audio', extensions: ['mp3', 'ogg', 'wav', 'm4a', 'flac'] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
];
|
|
}
|
|
if (mime === 'application/pdf' || ext === 'pdf') {
|
|
return [
|
|
{ name: 'PDF', extensions: ['pdf'] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
];
|
|
}
|
|
if (ext) {
|
|
return [
|
|
{ name: ext.toUpperCase(), extensions: [ext] },
|
|
{ name: 'All Files', extensions: ['*'] },
|
|
];
|
|
}
|
|
return [{ name: 'All Files', extensions: ['*'] }];
|
|
}
|
|
|
|
async function saveWithElectron(blob: Blob, filename: string): Promise<void> {
|
|
const media = (window.electron as { media?: ElectronMediaApi } | undefined)?.media;
|
|
if (!media?.saveFile) {
|
|
throw new Error('Electron media.saveFile is unavailable');
|
|
}
|
|
|
|
const data = await blobToUint8Array(blob);
|
|
const result = await media.saveFile({
|
|
filename,
|
|
mimeType: blob.type || 'application/octet-stream',
|
|
data,
|
|
});
|
|
|
|
if (result?.canceled) return;
|
|
if (result?.success === false) {
|
|
throw new Error(result.error || 'Failed to save file');
|
|
}
|
|
}
|
|
|
|
async function saveWithTauri(blob: Blob, filename: string): Promise<void> {
|
|
const { save } = await import('@tauri-apps/plugin-dialog');
|
|
const { writeFile } = await import('@tauri-apps/plugin-fs');
|
|
|
|
const path = await save({
|
|
defaultPath: filename,
|
|
filters: guessFilters(filename, blob.type),
|
|
});
|
|
if (!path) return;
|
|
|
|
const data = await blobToUint8Array(blob);
|
|
await writeFile(path, data);
|
|
}
|
|
|
|
/**
|
|
* Wire Electron / Tauri native save dialogs into core saveMedia helpers.
|
|
* Call once at desktop app startup. No-ops in plain browser.
|
|
*/
|
|
export function registerDesktopMediaSaver(): void {
|
|
if (isElectron()) {
|
|
registerMediaSaver(async (blob, filename) => {
|
|
await saveWithElectron(blob, filename);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isTauri()) {
|
|
registerMediaSaver(async (blob, filename) => {
|
|
await saveWithTauri(blob, filename);
|
|
});
|
|
}
|
|
}
|