Skip to content

Commit

Permalink
feat: add section data headers
Browse files Browse the repository at this point in the history
  • Loading branch information
billyjacoby committed Sep 25, 2023
1 parent 121c4b5 commit 0505212
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 37 deletions.
4 changes: 4 additions & 0 deletions src/assets/icons/downSquare.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import React from 'react';

import Video from 'react-native-video';

export const VideoPlayer = ({
videoURI,
isPaused = true,
snapshotURL,
}: {
videoURI: string;
isPaused?: boolean;
snapshotURL?: string;
}) => {
React.useEffect(() => {}, []);
return (
<Video
poster={snapshotURL}
posterResizeMode="cover"
className="w-full h-full rounded-md"
resizeMode="cover"
ignoreSilentSwitch="ignore"
paused={isPaused}
bufferConfig={{minBufferMs: 1000}}
fullscreen={true}
pictureInPicture={true}
controls
source={{uri: videoURI}}
Expand Down
15 changes: 9 additions & 6 deletions src/components/snapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Image, ImageURISource, useWindowDimensions, View} from 'react-native';

import clsx from 'clsx';

import {BaseText} from './baseText';
import {Label} from './label';
import {FrigateEvent} from '@api';
Expand All @@ -8,23 +10,24 @@ export const SnapshotCard = ({
imageSource,
camEvent,
imageOverlay,
addtlClasses,
}: {
imageSource: ImageURISource['uri'];
camEvent?: FrigateEvent & {lastEventEnded: string};
imageOverlay?: React.ReactNode;
addtlClasses?: string;
}) => {
const {width} = useWindowDimensions();
const imageWidth = width * 0.97;
const imageWidth = width;
const imageHeight = imageWidth * 0.75;

return (
<View className="self-center border border-accent dark:border-accent-dark relative rounded-lg">
<View className={clsx('rounded-xl', addtlClasses)}>
<Image
source={{uri: imageSource}}
resizeMode="contain"
// eslint-disable-next-line react-native/no-inline-styles
style={{height: imageHeight, width: imageWidth, borderRadius: 8}}
className="top-0"
resizeMode="cover"
style={{height: imageHeight, width: imageWidth}}
className="top-0 rounded-lg"
/>
{!!imageOverlay && imageOverlay}
{!!camEvent && (
Expand Down
94 changes: 77 additions & 17 deletions src/screens/EventsScreen/EventsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import {ActivityIndicator, FlatList, View} from 'react-native';
import React from 'react';
import {
ActivityIndicator,
LayoutAnimation,
SectionList,
View,
} from 'react-native';

import clsx from 'clsx';

import {CameraEvent} from './components';
import {useCameraEvents} from '@api';
import {FrigateEvent, useCameraEvents} from '@api';
import {BaseText, BaseView} from '@components';
import {useAppDataStore} from '@stores';
import {bgBackground} from '@utils';

const FooterComponent = ({length}: {length: number}) => (
//? I don't know why but we get some layout shift and that requires adding this height value here
<BaseView className="h-[250]">
<BaseText className="text-center text-mutedForeground dark:text-mutedForeground-dark pt-2">
Showing {length} event{length > 1 && 's'}.
</BaseText>
</BaseView>
);
import {SectionDateHeader} from './components/SectionDateHeader';

const FooterComponent = ({length}: {length?: number}) => {
return (
//? I don't know why but we get some layout shift and that requires adding this height value here
<BaseView className="">
{!!length && (
<BaseText className="text-center text-mutedForeground dark:text-mutedForeground-dark pt-2">
Showing {length} event{length > 1 && 's'}.
</BaseText>
)}
</BaseView>
);
};

export const EventsScreen = () => {
const currentCamera = useAppDataStore(state => state.currentCamera);
Expand All @@ -31,6 +43,36 @@ export const EventsScreen = () => {
{enabled: !!currentCamera},
);

const [collapsedSections, setCollapsedSections] = React.useState(new Set());

//? PERF: I could see this getting real expensive with more events. Consider moving into a RQ Select function?
const groupedEvents = React.useMemo(
() =>
events?.reduce<{title: string; data: FrigateEvent[]}[]>((acc, evt) => {
const evtDate = new Date(evt.start_time * 1000).toLocaleDateString();
const prevEventsIdx = acc.findIndex(grp => grp.title === evtDate);
const prevEvents = acc[prevEventsIdx]?.data;
if (prevEventsIdx > -1 && prevEvents?.length) {
acc[prevEventsIdx] = {title: evtDate, data: [...prevEvents, evt]};
} else {
acc.push({title: evtDate, data: [evt]});
}
return acc;
}, []),
[events],
);

const handleHeaderPress = (title: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
const newCollapsed = new Set(collapsedSections);
if (collapsedSections.has(title)) {
newCollapsed.delete(title);
} else {
newCollapsed.add(title);
}
setCollapsedSections(newCollapsed);
};

if (isLoading) {
return (
<BaseView>
Expand All @@ -39,7 +81,7 @@ export const EventsScreen = () => {
);
}

if (error || !events || (!isLoading && !currentCamera)) {
if (error || !groupedEvents || (!isLoading && !currentCamera)) {
return (
<BaseView>
<BaseText className="text-red-800 text-lg">
Expand All @@ -49,7 +91,7 @@ export const EventsScreen = () => {
);
}

if (!events.length) {
if (!groupedEvents?.length) {
return (
<BaseView isScrollview showsVerticalScrollIndicator={false}>
<BaseText>No events found.</BaseText>
Expand All @@ -59,14 +101,32 @@ export const EventsScreen = () => {

return (
<View className="flex-1">
<FlatList
<SectionList
className={clsx(bgBackground)}
ListFooterComponent={<FooterComponent length={events.length} />}
extraData={collapsedSections}
keyExtractor={(item, index) => item.id + index}
ListFooterComponent={<FooterComponent length={events?.length} />}
showsVerticalScrollIndicator={false}
data={events}
renderItem={({item: camEvent}) => (
<CameraEvent camEvent={camEvent} key={camEvent.id} />
sections={groupedEvents}
renderSectionHeader={props => (
<SectionDateHeader
{...props}
handleHeaderPress={handleHeaderPress}
isCollapsed={collapsedSections.has(props.section.title)}
/>
)}
renderItem={({item: camEvent, index, section}) => {
if (collapsedSections.has(section.title)) {
return null;
}
return (
<CameraEvent
camEvent={camEvent}
key={camEvent.id}
isFirst={index === 0}
/>
);
}}
/>
{/* // TODO: Get total event info and group by date. Add pagination heree */}
</View>
Expand Down
31 changes: 20 additions & 11 deletions src/screens/EventsScreen/components/CameraEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import {
useWindowDimensions,
} from 'react-native';

import clsx from 'clsx';

import {EventDetails} from './EventDetails';
import {FrigateEvent} from '@api';
import {BaseView, SnapshotCard, VideoPlayer} from '@components';

export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
export const CameraEvent = ({
camEvent,
isFirst,
}: {
camEvent: FrigateEvent;
isFirst?: boolean;
}) => {
const {width} = useWindowDimensions();
const imageWidth = width * 0.97;
const imageHeight = imageWidth * 0.75;
Expand Down Expand Up @@ -44,9 +52,7 @@ export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
if (scrollIndex === 2) {
setVideoIsPaused(false);
} else {
if (videoIsPaused === false) {
setVideoIsPaused(true);
}
setVideoIsPaused(true);
}
}
};
Expand All @@ -64,8 +70,7 @@ export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
contentOffset={{x: width, y: 0}}
showsHorizontalScrollIndicator={false}
pagingEnabled
style={{flex: 1}}
className="py-2">
className={clsx('py-2', isFirst && 'pt-1 rounded-t-none')}>
{/* //? Details on left */}
<BaseView
style={{width, height: imageHeight}}
Expand All @@ -86,11 +91,15 @@ export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
</BaseView>

{/* //? if there's a video, show that to the right */}
{camEvent.has_clip && (
<BaseView style={{width, height: imageHeight}} className="py-2">
<VideoPlayer videoURI={camEvent.vodURL} isPaused={videoIsPaused} />
</BaseView>
)}
<BaseView style={{width, height: imageHeight}}>
<VideoPlayer
key={camEvent.id}
videoURI={camEvent.vodURL}
isPaused={videoIsPaused}
snapshotURL={lastEventImage || lastThumbnail}
/>
</BaseView>
</ScrollView>
);
};
//
4 changes: 3 additions & 1 deletion src/screens/EventsScreen/components/EventDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export const EventDetails = ({camEvent}: {camEvent: FrigateEvent}) => {
</Row>
<Row>
<BaseText>Event Duration</BaseText>
<BaseText>{eventDuration} seconds</BaseText>
<BaseText>
{eventDuration > 0 ? eventDuration : 'In Progress'} seconds
</BaseText>
</Row>
<Row>
<BaseText>Object Label</BaseText>
Expand Down
40 changes: 40 additions & 0 deletions src/screens/EventsScreen/components/SectionDateHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {Pressable, useColorScheme, View} from 'react-native';

import DownSquare from '@icons/downSquare.svg';
import clsx from 'clsx';

import {FrigateEvent} from '@api';
import {BaseText, Label} from '@components';
import {getFgColorHex} from '@utils';

export const SectionDateHeader = ({
section,
handleHeaderPress,
isCollapsed,
}: {
section: {
title: string;
data: FrigateEvent[];
};
handleHeaderPress: (s: string) => void;
isCollapsed: boolean;
}) => {
const isDarkMode = useColorScheme() === 'dark';
return (
<Pressable onPress={() => handleHeaderPress(section.title)}>
<Label
className={clsx('px-4 py-2 rounded-t-none', isCollapsed && 'mb-1')}>
<View className="flex-row items-center justify-between">
<BaseText className="font-semibold">{section.title}</BaseText>
<DownSquare
fill={getFgColorHex(isDarkMode)}
height={28}
width={28}
style={isCollapsed && {transform: [{rotate: '180deg'}]}}
/>
</View>
</Label>
</Pressable>
);
};
File renamed without changes.
8 changes: 8 additions & 0 deletions src/utils/colors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {colors} from '../../themeColors';

export const hslToHex = (HSL: string) => {
HSL = HSL.replaceAll('%', '');
const [_h, _s, _l]: (string | number)[] = HSL.split(' ');
Expand All @@ -19,3 +21,9 @@ export const hslToHex = (HSL: string) => {
};

export const bgBackground = 'bg-background dark:bg-background-dark';
export const getFgColorHex = (isDarkMode: boolean) => {
if (isDarkMode) {
return hslToHex(colors.dark.foreground);
}
return hslToHex(colors.light.foreground);
};

0 comments on commit 0505212

Please sign in to comment.