Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
de096a3
Creating verse label
rafaelwinter01 Nov 25, 2025
71b2f9e
Updating TagModal model
rafaelwinter01 Nov 25, 2025
264975d
Adapting tags to assets
rafaelwinter01 Dec 3, 2025
e2d4a05
Fixing UI Onboard View Button alignment
rafaelwinter01 Dec 6, 2025
dc68e50
Merge remote-tracking branch 'origin/rafael/creating-verse-label' int…
rafaelwinter01 Dec 6, 2025
3a81d90
Add Assets Separators
rafaelwinter01 Dec 10, 2025
d5cc99d
Adapt to load only used tags
rafaelwinter01 Dec 10, 2025
341a784
Merge branch 'dev' into rafael/label-assets
rafaelwinter01 Dec 11, 2025
48505d5
Labelling assets
rafaelwinter01 Dec 12, 2025
e6f6504
Add new fields and functionalities related to labeling assets
rafaelwinter01 Dec 16, 2025
10e4695
Allow adding verses between assets
rafaelwinter01 Dec 17, 2025
99f7411
Merge branch 'dev' into rafael/label-assets-advanced
rafaelwinter01 Dec 18, 2025
f6528ad
Allow assign labels in the Recording View
rafaelwinter01 Dec 21, 2025
763556f
Fixing UI when moving assets
rafaelwinter01 Dec 21, 2025
17c0aa9
Added individual play button in assets list.
rafaelwinter01 Dec 22, 2025
32c006b
Add setting to allow user to choose use Verse Markers
rafaelwinter01 Jan 1, 2026
ab5e90d
Add prompt to ask user about using Verse Marker
rafaelwinter01 Jan 2, 2026
d8c0fa0
Change Question Modal to Drawer
rafaelwinter01 Jan 3, 2026
3326a1a
Add infinite scroll
rafaelwinter01 Jan 8, 2026
c5bf604
Merge branch 'dev' into rafael/label-assets-advanced-6
rafaelwinter01 Jan 9, 2026
158a8b1
Optimize functions
rafaelwinter01 Jan 9, 2026
63cc9bc
Modify Recording View to Bible Projects
rafaelwinter01 Jan 10, 2026
8520915
Enhancing ordering assets
rafaelwinter01 Jan 12, 2026
d89a1b5
Implemented Merge and Delete functions
rafaelwinter01 Jan 12, 2026
2d44058
Allow renaming assets on main assets view
rafaelwinter01 Jan 12, 2026
7f37d65
Allow Assigning Verses with Multi Select
rafaelwinter01 Jan 13, 2026
67ca034
Allow adding verses while recording
rafaelwinter01 Jan 14, 2026
f06c27b
Create bible verses in recording view
rafaelwinter01 Jan 15, 2026
b8989dd
Fix assets positioning
rafaelwinter01 Jan 15, 2026
14223f7
Remove Infinite Scroll when loading locally
rafaelwinter01 Jan 17, 2026
1171813
Refactor recording view placement function
rafaelwinter01 Jan 19, 2026
2e65692
Improve add verses logic
rafaelwinter01 Jan 20, 2026
aa47456
Improve performance
rafaelwinter01 Jan 20, 2026
515c255
Delete unused component
rafaelwinter01 Jan 20, 2026
94da055
Improving performance on the Recording View
rafaelwinter01 Jan 20, 2026
16078fb
Identify verse being recorded
rafaelwinter01 Jan 21, 2026
d42264d
Remove debug lines
rafaelwinter01 Jan 21, 2026
e2f8b1e
Enhance UI experience
rafaelwinter01 Jan 23, 2026
acf4d04
Fix single asset play
rafaelwinter01 Jan 23, 2026
8ceecf0
Improving Deletion Drawer
rafaelwinter01 Jan 23, 2026
c13b92e
Lint update
rafaelwinter01 Jan 23, 2026
05f9857
Improving performance
rafaelwinter01 Jan 24, 2026
ba2147d
Improving performance
rafaelwinter01 Jan 24, 2026
beb5cf4
Merge branch 'dev' into rafael/label-assets-advanced-6.5
rafaelwinter01 Jan 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export default function RootLayout() {
});

useEffect(() => {
// async function init() {
// await tagService.preloadTagsIntoCache();
// }
// void init();

if (Platform.OS === 'web') return;
console.log('[_layout] Setting up deep link handler');

Expand Down
37 changes: 37 additions & 0 deletions components/AddVerseLabelButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PlusCircleIcon } from 'lucide-react-native';
import React from 'react';
import { Pressable, View } from 'react-native';
import { Icon } from './ui/icon';
import { Text } from './ui/text';

interface AddVerseLabelButtonProps {
onPress: () => void;
disabled?: boolean;
className?: string;
}

export function AddVerseLabelButton({
onPress,
disabled = false,
className = ''
}: AddVerseLabelButtonProps) {
return (
<View className={`-mb-3 w-full flex-row items-center ${className}`}>
{/* <View className="h-px flex-1 bg-primary/10" /> */}
<View className="h-px flex-1" />
<Pressable
onPress={onPress}
disabled={disabled}
className={`mx-1.5 flex-row items-center gap-1.5 rounded-full border border-dashed border-primary/30 px-2.5 active:bg-primary/10 ${
disabled ? 'opacity-40' : ''
}`}
>
<Icon as={PlusCircleIcon} size={12} className="text-primary/70" />
<Text className="text-[10.5px] font-semibold text-primary/70">
Add verse
</Text>
</Pressable>
{/* <View className="h-px flex-1 bg-primary/10" /> */}
</View>
);
}
136 changes: 90 additions & 46 deletions components/ArrayInsertionWheel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,66 @@ export interface ArrayInsertionWheelHandle {
scrollItemToTop: (index: number, animated?: boolean) => void;
}

interface ArrayInsertionWheelProps {
children: React.ReactNode[];
interface ArrayInsertionWheelPropsBase {
value: number; // 0..N insertion boundary
onChange?: (index: number) => void;
rowHeight: number;
className?: string;
topInset?: number; // unused in native wheel, kept for API parity
bottomInset?: number; // unused in native wheel, kept for API parity
boundaryComponent?: React.ReactNode;
}

// API 1: Eager rendering with children (backward compatible)
interface ArrayInsertionWheelPropsEager extends ArrayInsertionWheelPropsBase {
children: React.ReactNode[];
data?: never;
renderItem?: never;
}

// API 2: Lazy rendering with data + renderItem (optimized)
interface ArrayInsertionWheelPropsLazy<T> extends ArrayInsertionWheelPropsBase {
children?: never;
data: T[];
renderItem: (item: T, index: number) => React.ReactElement;
}

function ArrayInsertionWheelInternal(
{
children,
type ArrayInsertionWheelProps<T = unknown> =
| ArrayInsertionWheelPropsEager
| ArrayInsertionWheelPropsLazy<T>;

function ArrayInsertionWheelInternal<T>(
props: ArrayInsertionWheelProps<T>,
ref: React.Ref<ArrayInsertionWheelHandle>
) {
const {
value,
onChange,
rowHeight,
className,
topInset = 0,
bottomInset = 0
}: ArrayInsertionWheelProps,
ref: React.Ref<ArrayInsertionWheelHandle>
) {
const itemCount = children.length + 1; // extra end boundary
bottomInset = 0,
boundaryComponent
} = props;

// Determine which API is being used
const isLazyMode = 'data' in props && props.data !== undefined;

// Calculate item count based on mode
let itemCount: number;
if (isLazyMode) {
itemCount = props.data.length + 1;
} else {
// Eager mode - children is guaranteed by type
itemCount = props.children.length + 1;
}

const clampedValue = Math.max(0, Math.min(itemCount - 1, value));

// Stabilize clampedValue to prevent unnecessary WheelPicker updates
const prevClampedRef = React.useRef(clampedValue);
const stableClampedValue = React.useMemo(() => {
if (prevClampedRef.current !== clampedValue) {
console.log(
'📊 Wheel value changed:',
prevClampedRef.current,
'→',
clampedValue,
'| itemCount:',
itemCount
);
prevClampedRef.current = clampedValue;
}
return clampedValue;
}, [clampedValue, itemCount]);
}, [clampedValue]);

// Debug logging to trace clamping
React.useEffect(() => {
Expand All @@ -67,7 +86,7 @@ function ArrayInsertionWheelInternal(
}
}, [value, clampedValue, itemCount]);

const data = React.useMemo<PickerItem<number>[]>(
const pickerData = React.useMemo<PickerItem<number>[]>(
() => Array.from({ length: itemCount }, (_, i) => ({ value: i })),
[itemCount]
);
Expand Down Expand Up @@ -116,22 +135,50 @@ function ArrayInsertionWheelInternal(
[stableClampedValue, itemCount, onChange]
);

const renderItem = React.useCallback(
({ item }: { item: PickerItem<number> }) => {
const renderItemInternal = React.useCallback(
({ item }: { item: PickerItem<number> }): React.ReactElement => {
const i = item.value;

// Calculate data length based on mode
let dataLength: number;
if (isLazyMode) {
dataLength = props.data.length;
} else {
dataLength = props.children.length;
}

// Render actual items (not the final boundary)
if (i < children.length) {
return (
<View style={{ height: rowHeight, justifyContent: 'center' }}>
{children[i]}
</View>
);
if (i < dataLength) {
if (isLazyMode) {
// Lazy mode: call renderItem with data item
const dataItem = props.data[i];
if (!dataItem) {
// Defensive: should never happen, but TypeScript needs this
return <View style={{ height: rowHeight }} />;
}
return (
<View style={{ height: rowHeight, justifyContent: 'center' }}>
{props.renderItem(dataItem, i)}
</View>
);
} else {
// Eager mode: use pre-created children
const child = props.children[i];
return (
<View style={{ height: rowHeight, justifyContent: 'center' }}>
{child}
</View>
);
}
}

// Final boundary (i === children.length)
// Final boundary (i === dataLength)
// When empty (0 items), this is position 0 - the only insertion point
// When non-empty, this is position N - insert after all items
if (boundaryComponent) {
return <>{boundaryComponent}</>;
}

return (
<View
style={{ height: rowHeight }}
Expand Down Expand Up @@ -160,7 +207,7 @@ function ArrayInsertionWheelInternal(
</View>
);
},
[children, rowHeight]
[isLazyMode, props, rowHeight, boundaryComponent]
);

return (
Expand All @@ -174,15 +221,15 @@ function ArrayInsertionWheelInternal(
onLayout={onContainerLayout}
>
<WheelPicker
data={data}
data={pickerData}
value={stableClampedValue}
itemHeight={rowHeight}
visibleItemCount={visibleCount}
// Fire only on final change to avoid thrashing parent state during scroll
onValueChanged={({ item }) => onChange?.(item.value)}
// Constrain height to an exact multiple of rowHeight so overlay aligns
style={{ height: wheelHeight }}
renderItem={renderItem}
renderItem={renderItemInternal}
renderItemContainer={({ key, ...props }) => (
<ArrayInsertionWheelContainer key={key} {...props} />
)}
Expand All @@ -209,13 +256,10 @@ function ArrayInsertionWheelInternal(
);
}

export default React.forwardRef<
ArrayInsertionWheelHandle,
ArrayInsertionWheelProps
>(
ArrayInsertionWheelInternal as unknown as (
props: ArrayInsertionWheelProps & {
ref?: React.Ref<ArrayInsertionWheelHandle>;
}
) => React.ReactElement
);
const ArrayInsertionWheel = React.forwardRef(ArrayInsertionWheelInternal) as <T = unknown>(
props: ArrayInsertionWheelProps<T> & {
ref?: React.Ref<ArrayInsertionWheelHandle>;
}
) => React.ReactElement;

export default ArrayInsertionWheel;
125 changes: 125 additions & 0 deletions components/AssetsDeletionDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer';
import { Input } from '@/components/ui/input';
import { Text } from '@/components/ui/text';
import { useLocalization } from '@/hooks/useLocalization';
import { AlertTriangleIcon } from 'lucide-react-native';
import React from 'react';
import { View } from 'react-native';
import { Icon } from './ui/icon';

interface AssetsDeletionDrawerProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void | Promise<void>;
title: string;
description: string;
confirmationString: string; // String that user must type to confirm deletion
}

export const AssetsDeletionDrawer: React.FC<AssetsDeletionDrawerProps> = ({
isOpen,
onClose,
onConfirm,
title,
description,
confirmationString
}) => {
const { t } = useLocalization();
const [inputValue, setInputValue] = React.useState('');
const [isExecuting, setIsExecuting] = React.useState(false);

// Reset input when drawer opens
React.useEffect(() => {
if (isOpen) {
setInputValue('');
setIsExecuting(false);
}
}, [isOpen]);

const handleConfirm = async () => {
if (inputValue !== confirmationString || isExecuting) return;

setIsExecuting(true);
try {
await onConfirm();
onClose();
} catch (error) {
console.error('Error executing deletion:', error);
} finally {
setIsExecuting(false);
}
};

const isButtonDisabled = inputValue !== confirmationString || isExecuting;

return (
<Drawer open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DrawerContent>
<DrawerHeader className="items-center">
<View className="mb-4 rounded-full bg-destructive/10 p-4">
<Icon
as={AlertTriangleIcon}
size={32}
className="text-destructive"
/>
</View>
<DrawerTitle className="text-center text-xl">{title}</DrawerTitle>
<DrawerDescription className="text-center">
{description}
</DrawerDescription>
</DrawerHeader>

<View className="px-4 pb-4">
<Text className="mb-2 text-sm text-muted-foreground">
{t('typeToConfirm').replace(
'{text}',
`"${confirmationString}"`
)}
</Text>
<Input
value={inputValue}
onChangeText={setInputValue}
placeholder={confirmationString}
autoCapitalize="none"
autoCorrect={false}
editable={!isExecuting}
/>
</View>

<DrawerFooter className="gap-2">
<Button
variant="destructive"
onPress={handleConfirm}
disabled={isButtonDisabled}
className={isButtonDisabled ? 'opacity-50' : ''}
>
{isExecuting ? (
<Text className="font-semibold text-destructive-foreground">
{t('deleting')}
</Text>
) : (
<Text className="font-semibold text-destructive-foreground">
{t('confirmDeletion')}
</Text>
)}
</Button>

<DrawerClose asChild>
<Button variant="outline" disabled={isExecuting}>
<Text className="font-semibold">{t('cancel')}</Text>
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
};
Loading
Loading