Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: event section list #9

Merged
merged 2 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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