Skip to content

Commit

Permalink
feat: event section list (#9)
Browse files Browse the repository at this point in the history
* chore: adjust lib versions

* feat: add section data headers
  • Loading branch information
billyjacoby authored Sep 25, 2023
1 parent f4e192e commit 04450c0
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 58 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"tailwindCSS.classAttributes": ["className", "tw", "addtlClasses"]}
16 changes: 4 additions & 12 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ PODS:
- MMKVCore (~> 1.3.1)
- MMKVCore (1.3.1)
- OpenSSL-Universal (1.1.1100)
- PromisesObjC (2.3.1)
- PromisesSwift (2.3.1):
- PromisesObjC (= 2.3.1)
- RCT-Folly (2021.07.22.00):
- boost
- DoubleConversion
Expand Down Expand Up @@ -387,11 +384,10 @@ PODS:
- React-Core
- react-native-safe-area-context (4.7.1):
- React-Core
- react-native-video (6.0.0-alpha.8):
- react-native-video (5.2.1):
- React-Core
- react-native-video/Video (= 6.0.0-alpha.8)
- react-native-video/Video (6.0.0-alpha.8):
- PromisesSwift
- react-native-video/Video (= 5.2.1)
- react-native-video/Video (5.2.1):
- React-Core
- react-native-webrtc (111.0.3):
- JitsiWebRTC (~> 111.0.0)
Expand Down Expand Up @@ -637,8 +633,6 @@ SPEC REPOS:
- MMKV
- MMKVCore
- OpenSSL-Universal
- PromisesObjC
- PromisesSwift
- SocketRocket
- YogaKit

Expand Down Expand Up @@ -761,8 +755,6 @@ SPEC CHECKSUMS:
MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7
MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: c0569ecc035894e4a68baecb30fe6a7ea6e399f9
RCTTypeSafety: e90354072c21236e0bcf1699011e39acd25fea2f
Expand All @@ -780,7 +772,7 @@ SPEC CHECKSUMS:
React-logger: da1ebe05ae06eb6db4b162202faeafac4b435e77
react-native-mmkv: 9ae7ca3977e8ef48dbf7f066974eb844c20b5fd7
react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2
react-native-video: 86950ad481cec184d7c9420ec3bca0c27904bbcd
react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253
react-native-webrtc: 4d1669c2ed29767fe70b0169428b4466589ecf8b
React-NativeModulesApple: edb5ace14f73f4969df6e7b1f3e41bef0012740f
React-perflogger: 496a1a3dc6737f964107cb3ddae7f9e265ddda58
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"react-native-safe-area-context": "^4.7.1",
"react-native-screens": "^3.24.0",
"react-native-svg": "^13.13.0",
"react-native-video": "alpha",
"react-native-video": "latest",
"react-native-webrtc": "^111.0.3",
"react-query": "^3.39.3",
"superjson": "^1.13.1",
Expand Down
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.
Loading

0 comments on commit 04450c0

Please sign in to comment.