Add the CREATE RECIPES tab to the web app
The phone's RecipeCreateModal becomes a rail tab with the same rows, seeding from the look on screen and clamping the same way. SAVE RECIPE applies the new look, lists it under RECIPES and, when signed in, stores it on the account; a guest's copy stays in memory and goes away with the page. Signed-in users can also export the recipe as the app's encrypted .recipe file (shared/utils /recipeShare.ts vendored byte-identical from the RN project).
This commit is contained in:
@@ -4,6 +4,7 @@ import { ChipRow, SliderRow, type ChipDef, type SliderDef } from './ui/ChipColum
|
||||
import { ImageStage } from './ui/ImageStage';
|
||||
import { TopBar } from './ui/TopBar';
|
||||
import { AuthModal } from './ui/AuthModal';
|
||||
import { RecipeCreatePanel, type RecipeDraft } from './ui/RecipeCreatePanel';
|
||||
import { useI18n } from './i18n/I18nProvider';
|
||||
import { loadEngine } from './engine/assets';
|
||||
import { renderPhoto, type RenderAssets } from './engine/exportEngine';
|
||||
@@ -12,6 +13,7 @@ import { api, type SavedRecipe, type User } from './api';
|
||||
import { TIERS } from './config/tiers';
|
||||
import { DEFAULT_ADJUSTMENTS, DEFAULT_RECIPES, FILM_SIMS, filmSimToRecipe } from '../shared/utils/defaultRecipes';
|
||||
import { PARAM_DEFS, type ContinuousParamDef } from '../shared/utils/paramDefs';
|
||||
import { exportRecipeXml } from '../shared/utils/recipeShare';
|
||||
import { writeJpegExif } from '../shared/utils/exifWrite';
|
||||
import { CROP_W_H, type CropRatio, type FrameId, type GPSInfo, type Recipe } from '../shared/types';
|
||||
import type { MsgKey } from './i18n/vi';
|
||||
@@ -120,6 +122,9 @@ export function Workspace() {
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [saved, setSaved] = useState<SavedRecipe[]>([]);
|
||||
// Recipes made in this session, guest ones included. A guest's copy lives
|
||||
// here and nowhere else, so reloading the page is what deletes it.
|
||||
const [created, setCreated] = useState<Recipe[]>([]);
|
||||
// null = closed. The landing's register link is /app?auth=1, so it opens the
|
||||
// dialog straight on the sign-up tab.
|
||||
const [authMode, setAuthMode] = useState<'login' | 'signup' | null>(() =>
|
||||
@@ -300,6 +305,41 @@ export function Workspace() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [recipe, user, t]);
|
||||
|
||||
// CREATE tab: a new recipe is applied at once and listed under RECIPES. A
|
||||
// signed-in account also stores it; a guest's copy stays in `created`, which
|
||||
// is exactly what "forgotten when you leave" means.
|
||||
const addCreatedRecipe = useCallback(
|
||||
async (draft: RecipeDraft) => {
|
||||
const entry: Recipe = { ...draft, id: `local-${Date.now()}`, isCustom: true };
|
||||
setCreated((list) => [entry, ...list]);
|
||||
applyRecipe(entry);
|
||||
if (!user) return;
|
||||
try {
|
||||
const stored = await api.createRecipe(entry.name, entry);
|
||||
setSaved((list) => [stored.recipe, ...list]);
|
||||
} catch (err) {
|
||||
setError(t('err.generic', { msg: String(err) }));
|
||||
}
|
||||
},
|
||||
[user, applyRecipe, t]
|
||||
);
|
||||
|
||||
// The phone's encrypted .recipe envelope (`shared/utils/recipeShare` is the
|
||||
// app's own file, byte-identical). Signed in only — a guest's recipe is never
|
||||
// written to disk.
|
||||
const exportRecipeFile = useCallback(
|
||||
(draft: RecipeDraft) => {
|
||||
if (!user) return;
|
||||
const xml = exportRecipeXml({ ...draft, id: 'share' });
|
||||
download(
|
||||
new TextEncoder().encode(xml),
|
||||
`${(draft.name || 'recipe').replace(/\W+/g, '-').toLowerCase()}.recipe`,
|
||||
'application/xml'
|
||||
);
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const exportPhoto = useCallback(async () => {
|
||||
if (!engine || !source) return;
|
||||
setExporting(true);
|
||||
@@ -660,6 +700,9 @@ export function Workspace() {
|
||||
return isWmPanel(openGroup)
|
||||
? [{ key: 'wm-back', label: '< FRAME', onClick: () => setOpenGroup(null) }, ...wmChips()]
|
||||
: framesRow();
|
||||
// CREATE draws its own form (ui/RecipeCreatePanel) instead of a chip row.
|
||||
case 'create':
|
||||
return [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -671,12 +714,21 @@ export function Workspace() {
|
||||
...choiceChips(g, groupDefs[g].options, groupDefs[g].value, groupDefs[g].onPick),
|
||||
];
|
||||
|
||||
const recipeStrip: ChipDef[] = DEFAULT_RECIPES.map((preset) => ({
|
||||
key: preset.id,
|
||||
label: preset.name,
|
||||
active: recipe.name === preset.name && !recipe.isCustom,
|
||||
onClick: () => applyRecipe({ ...preset }),
|
||||
}));
|
||||
// RECIPES = what this session made (newest first) + the bundled presets.
|
||||
const recipeStrip: ChipDef[] = [
|
||||
...created.map((entry) => ({
|
||||
key: entry.id,
|
||||
label: entry.name,
|
||||
active: recipe.id === entry.id,
|
||||
onClick: () => applyRecipe(entry),
|
||||
})),
|
||||
...DEFAULT_RECIPES.map((preset) => ({
|
||||
key: preset.id,
|
||||
label: preset.name,
|
||||
active: recipe.name === preset.name && !recipe.isCustom,
|
||||
onClick: () => applyRecipe({ ...preset }),
|
||||
})),
|
||||
];
|
||||
|
||||
const status = busy ? busy : rendered ? t('meta.rendered', { ms: rendered.ms, w: rendered.w, h: rendered.h }) : t('meta.ready');
|
||||
|
||||
@@ -723,7 +775,18 @@ export function Workspace() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 4. FRAME -> WATERMARK sub-panel: the mark's own text + place */}
|
||||
{/* 4. CREATE -> the phone's recipe form */}
|
||||
{tab === 'create' ? (
|
||||
<RecipeCreatePanel
|
||||
currentBase={recipe.baseFilter}
|
||||
current={recipe.adjustments}
|
||||
signedIn={!!user}
|
||||
onSave={(draft) => void addCreatedRecipe(draft)}
|
||||
onExport={exportRecipeFile}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* 5. FRAME -> WATERMARK sub-panel: the mark's own text + place */}
|
||||
{tab === 'frame' && isWmPanel(openGroup) ? (
|
||||
<div className="strip wm-extra">
|
||||
{markOn ? (
|
||||
@@ -775,7 +838,7 @@ export function Workspace() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 5. main chip row, RESET pinned outside the scroll */}
|
||||
{/* 6. main chip row, RESET pinned outside the scroll */}
|
||||
<div className="main-row">
|
||||
<ChipRow chips={[resetChip()]} />
|
||||
<ChipRow chips={chips} />
|
||||
|
||||
Reference in New Issue
Block a user