feat(studio): hand an export straight to a social network

EXPORT only ever wrote a file, and posting it meant leaving the studio, finding
the file and uploading it again. The dialog now offers FACEBOOK, INSTAGRAM,
REDDIT and X beside the sizes, and the render goes to the network instead of the
download folder.

What the browser can do decides how, since a page may not touch another origin's
composer: a phone's share sheet opens the target app with the picture already
attached, a desktop gets the picture on the clipboard and the site in a new tab
for one paste, and an origin with no clipboard API downloads the file beside the
open site. The line under the row says so before the click.
This commit is contained in:
2026-10-08 14:38:22 +07:00
parent f1d7eeff99
commit 8f286506b2
6 changed files with 139 additions and 2 deletions
+11 -2
View File
@@ -17,6 +17,7 @@ import { loadEngine, MARK_FACES } from './engine/assets';
import { renderPhoto, type RenderAssets } from './engine/exportEngine';
import { download, fileToBytes, readCapturedAt, readGps, readIso, readSpecs, resizedJpeg, scaledJpeg, type ShotSpecs } from './engine/imageOps';
import { developRaw, isRawName } from './engine/rawDevelop';
import { sharePhoto, type ShareSite } from './engine/share';
import { heicToJpeg, isHeicName } from './engine/heicDevelop';
import { clearRaw, readRaw, storeRaw } from './engine/rawStore';
import { getPhoto, loadEdit, readPhotoFile, saveEdit, setRotation as savePhotoRotation } from './engine/library';
@@ -1748,7 +1749,7 @@ export function Workspace() {
// `true` when a file was handed over, so CLEAR knows the export it offered
// actually happened before the photo comes off the stage.
const exportPhoto = useCallback(async (target: number | null): Promise<boolean> => {
const exportPhoto = useCallback(async (target: number | null, share?: ShareSite): Promise<boolean> => {
if (!engine || !source) return false;
if (!pro && proLookInUse()) {
promptPro();
@@ -1833,7 +1834,11 @@ export function Workspace() {
source.bytes
);
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
download(bytes, `recipescam_${(recipe.name || 'photo').replace(/\W+/g, '-').toLowerCase()}_${stamp}.jpg`);
const name = `recipescam_${(recipe.name || 'photo').replace(/\W+/g, '-').toLowerCase()}_${stamp}.jpg`;
// SHARE hands the same bytes to the browser's share sheet instead of the
// download folder, so the file never lands on the machine on the way out.
if (share) await sharePhoto(bytes, name, share);
else download(bytes, name);
// The moment the app has done the visitor's work is the moment it has earned
// a place on the machine: the install offer waits for this instead of
// opening on the way in (which nobody reads).
@@ -4141,6 +4146,10 @@ export function Workspace() {
setChoosingExport(false);
void exportPhoto(target);
}}
onShare={(site) => {
setChoosingExport(false);
void exportPhoto(null, site);
}}
onClose={() => setChoosingExport(false)}
/>
) : null}