feat(panel): add a FAVORITED tab with a star on every recipe
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Trash2 } from 'lucide-react-native';
|
||||
import { Star, Trash2 } from 'lucide-react-native';
|
||||
import { Skia } from '@shopify/react-native-skia';
|
||||
import { Recipe, ColorAdjustments, CustomWatermark, GpsWatermark, FrameId, BaseFilter, CropRatio } from '../types';
|
||||
import { FRAMES } from '../utils/frameUtils';
|
||||
@@ -16,7 +16,7 @@ import RecipeCreateModal from './RecipeCreateModal';
|
||||
// Tapping a parameter chip toggles its slider row; chips stay visible.
|
||||
// Darkroom-flavoured: amber = active/edited, red = destructive, mono labels.
|
||||
|
||||
type TabId = 'recipes' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
|
||||
type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
|
||||
// Choice-group keys that open a strip above the chip row: color chrome groups
|
||||
// (wb tab) and the D.RANGE selector (iq tab).
|
||||
type GroupKey =
|
||||
@@ -73,6 +73,10 @@ interface AdjustmentPanelProps {
|
||||
// save-as (new name).
|
||||
onUpdateRecipe: (recipe: Omit<Recipe, 'id' | 'isCustom'>) => void;
|
||||
onSelectRecipe: (recipe: Recipe) => void;
|
||||
// FAVORITED tab + the star trailing every recipe chip. Ids only: the star is
|
||||
// metadata, so it survives RESET and never changes the current look.
|
||||
favoriteIds: string[];
|
||||
onToggleFavorite: (id: string) => void;
|
||||
onUpdateAdjustments: (adjustments: Partial<ColorAdjustments>) => void;
|
||||
onUpdateFrame: (frameId: FrameId) => void;
|
||||
// FRAME tab: turn the inserted library photo 90deg (landscape <-> portrait)
|
||||
@@ -116,6 +120,9 @@ interface ChipDef {
|
||||
onPress: () => void;
|
||||
onPressDelete?: () => void; // custom presets carry an inline trash button
|
||||
trash?: boolean;
|
||||
star?: boolean; // trailing star button (recipes + FAVORITED)
|
||||
starred?: boolean; // filled/amber star
|
||||
onPressStar?: () => void;
|
||||
disabled?: boolean; // informational label, not tappable
|
||||
amberValue?: boolean; // amber text driven by edited value, not selection
|
||||
}
|
||||
@@ -196,6 +203,8 @@ export default function AdjustmentPanel({
|
||||
onCloseEdit,
|
||||
onUpdateRecipe,
|
||||
onSelectRecipe,
|
||||
favoriteIds,
|
||||
onToggleFavorite,
|
||||
onUpdateAdjustments,
|
||||
onUpdateFrame,
|
||||
photoRotation,
|
||||
@@ -277,22 +286,41 @@ export default function AdjustmentPanel({
|
||||
const textTone = c.active || c.amberValue ? 'text-amber-500' : 'text-zinc-300';
|
||||
return (
|
||||
<View key={c.key} className="mx-1">
|
||||
{c.trash ? (
|
||||
{c.trash || c.star ? (
|
||||
<View className={`flex-row items-center rounded-full border py-1.5 pl-3 pr-1 ${tone}`}>
|
||||
<TouchableOpacity onPress={() => { haptic(); c.onPress(); }} activeOpacity={0.7}>
|
||||
<Text className={`font-mono text-[11px] font-bold ${textTone}`}>{c.label}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
haptic();
|
||||
c.onPressDelete && c.onPressDelete();
|
||||
}}
|
||||
className="p-1.5 ml-1"
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Trash2 size={12} color="#ef4444" />
|
||||
</TouchableOpacity>
|
||||
{c.star && (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
haptic();
|
||||
c.onPressStar && c.onPressStar();
|
||||
}}
|
||||
className="p-1.5 ml-1"
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Star
|
||||
size={12}
|
||||
color={c.starred ? '#f59e0b' : '#a1a1aa'}
|
||||
fill={c.starred ? '#f59e0b' : 'none'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{c.trash && (
|
||||
<TouchableOpacity
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
haptic();
|
||||
c.onPressDelete && c.onPressDelete();
|
||||
}}
|
||||
className="p-1.5 ml-1"
|
||||
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
||||
>
|
||||
<Trash2 size={12} color="#ef4444" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : c.disabled ? (
|
||||
<View className="mx-1 flex-row items-center rounded-full border border-zinc-800/80 bg-black/30 px-3 py-1.5">
|
||||
@@ -372,6 +400,7 @@ export default function AdjustmentPanel({
|
||||
}));
|
||||
const paramDefs: Record<TabId, ParamDef[]> = {
|
||||
recipes: [],
|
||||
favorites: [],
|
||||
iq: toRows(PARAM_DEFS.iq),
|
||||
wb: toRows(PARAM_DEFS.wb).map((p) =>
|
||||
p.key === 'temperature'
|
||||
@@ -589,6 +618,39 @@ export default function AdjustmentPanel({
|
||||
);
|
||||
}
|
||||
|
||||
case 'favorites': {
|
||||
// FAVORITED: every starred recipe as a chip (star to un-star), newest
|
||||
// look selectable in one tap. Ids whose recipe was deleted simply drop.
|
||||
const favs = recipes.filter((r) => favoriteIds.includes(r.id));
|
||||
return (
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8 }}
|
||||
>
|
||||
{favs.length === 0
|
||||
? renderChip({
|
||||
key: 'no-favorites',
|
||||
label: 'NO FAVORITES YET - TAP THE STAR ON A RECIPE',
|
||||
disabled: true,
|
||||
active: false,
|
||||
onPress: () => {},
|
||||
})
|
||||
: favs.map((r) =>
|
||||
renderChip({
|
||||
key: r.id,
|
||||
label: r.name,
|
||||
active: r.id === currentRecipeId,
|
||||
star: true,
|
||||
starred: true,
|
||||
onPress: () => onSelectRecipe(r),
|
||||
onPressStar: () => onToggleFavorite(r.id),
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
case 'iq':
|
||||
// LIGHT: continuous params on one row; D.RANGE is a toggle chip that
|
||||
// opens its AUTO/DR100/DR200/DR400 strip above (same as the chrome
|
||||
@@ -799,8 +861,11 @@ export default function AdjustmentPanel({
|
||||
key: r.id,
|
||||
label: r.name,
|
||||
active: r.id === currentRecipeId,
|
||||
star: true,
|
||||
starred: favoriteIds.includes(r.id),
|
||||
trash: !!r.isCustom,
|
||||
onPress: () => onSelectRecipe(r),
|
||||
onPressStar: () => onToggleFavorite(r.id),
|
||||
onPressDelete: () => onDeleteRecipe(r.id),
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
|
||||
export type TabId = 'recipes' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
|
||||
export type TabId = 'recipes' | 'favorites' | 'iq' | 'wb' | 'filters' | 'frame' | 'watermark';
|
||||
|
||||
interface ToolRailProps {
|
||||
activeTab: TabId | null;
|
||||
@@ -14,6 +14,7 @@ interface ToolRailProps {
|
||||
|
||||
const TOOLS: { id: TabId; label: string }[] = [
|
||||
{ id: 'recipes', label: 'PRESETS' },
|
||||
{ id: 'favorites', label: 'FAVORITED' },
|
||||
{ id: 'iq', label: 'LIGHT' },
|
||||
{ id: 'wb', label: 'WB' },
|
||||
{ id: 'filters', label: 'FX' },
|
||||
|
||||
@@ -3,6 +3,29 @@ import { Recipe } from '../types';
|
||||
import { DEFAULT_RECIPES, DEFAULT_ADJUSTMENTS } from './defaultRecipes';
|
||||
|
||||
const CUSTOM_RECIPES_KEY = '@camrecipe_pro:custom_recipes';
|
||||
const FAVORITES_KEY = '@camrecipe_pro:favorites';
|
||||
|
||||
// FAVORITED recipes: a list of recipe ids. Pure metadata — starring a recipe
|
||||
// never touches the look, so RESET and re-selecting a recipe leave it alone.
|
||||
export async function getFavoriteIds(): Promise<string[]> {
|
||||
try {
|
||||
const data = await AsyncStorage.getItem(FAVORITES_KEY);
|
||||
if (!data) return [];
|
||||
const parsed = JSON.parse(data);
|
||||
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : [];
|
||||
} catch (error) {
|
||||
console.error('Failed to load favorites:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function setFavoriteId(id: string, on: boolean): Promise<void> {
|
||||
const ids = await getFavoriteIds();
|
||||
const updated = on
|
||||
? [...new Set([...ids, id])]
|
||||
: ids.filter((x) => x !== id);
|
||||
await AsyncStorage.setItem(FAVORITES_KEY, JSON.stringify(updated));
|
||||
}
|
||||
|
||||
export async function getCustomRecipes(): Promise<Recipe[]> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user