Event.jsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { h, Fragment } from 'preact';
  2. import { useCallback, useState } from 'preact/hooks';
  3. import { route } from 'preact-router';
  4. import ActivityIndicator from '../components/ActivityIndicator';
  5. import Button from '../components/Button';
  6. import Clip from '../icons/Clip';
  7. import Delete from '../icons/Delete';
  8. import Snapshot from '../icons/Snapshot';
  9. import Dialog from '../components/Dialog';
  10. import Heading from '../components/Heading';
  11. import Link from '../components/Link';
  12. import VideoPlayer from '../components/VideoPlayer';
  13. import { FetchStatus, useApiHost, useEvent } from '../api';
  14. import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
  15. export default function Event({ eventId }) {
  16. const apiHost = useApiHost();
  17. const { data, status } = useEvent(eventId);
  18. const [showDialog, setShowDialog] = useState(false);
  19. const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
  20. const handleClickDelete = () => {
  21. setShowDialog(true);
  22. };
  23. const handleDismissDeleteDialog = () => {
  24. setShowDialog(false);
  25. };
  26. const handleClickDeleteDialog = useCallback(async () => {
  27. let success;
  28. try {
  29. const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' });
  30. success = await (response.status < 300 ? response.json() : { success: true });
  31. setDeleteStatus(success ? FetchStatus.LOADED : FetchStatus.ERROR);
  32. } catch (e) {
  33. setDeleteStatus(FetchStatus.ERROR);
  34. }
  35. if (success) {
  36. setDeleteStatus(FetchStatus.LOADED);
  37. setShowDialog(false);
  38. route('/events', true);
  39. }
  40. }, [apiHost, eventId, setShowDialog]);
  41. if (status !== FetchStatus.LOADED) {
  42. return <ActivityIndicator />;
  43. }
  44. const startime = new Date(data.start_time * 1000);
  45. const endtime = new Date(data.end_time * 1000);
  46. return (
  47. <div className="space-y-4">
  48. <div className="flex">
  49. <Heading className="flex-grow">
  50. {data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
  51. </Heading>
  52. <Button className="self-start" color="red" onClick={handleClickDelete}>
  53. <Delete className="w-6" /> Delete event
  54. </Button>
  55. {showDialog ? (
  56. <Dialog
  57. onDismiss={handleDismissDeleteDialog}
  58. title="Delete Event?"
  59. text="This event will be permanently deleted along with any related clips and snapshots"
  60. actions={[
  61. deleteStatus !== FetchStatus.LOADING
  62. ? { text: 'Delete', color: 'red', onClick: handleClickDeleteDialog }
  63. : { text: 'Deleting…', color: 'red', disabled: true },
  64. { text: 'Cancel', onClick: handleDismissDeleteDialog },
  65. ]}
  66. />
  67. ) : null}
  68. </div>
  69. <Table class="w-full">
  70. <Thead>
  71. <Th>Key</Th>
  72. <Th>Value</Th>
  73. </Thead>
  74. <Tbody>
  75. <Tr>
  76. <Td>Camera</Td>
  77. <Td>
  78. <Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
  79. </Td>
  80. </Tr>
  81. <Tr index={1}>
  82. <Td>Timeframe</Td>
  83. <Td>
  84. {startime.toLocaleString()} – {endtime.toLocaleString()}
  85. </Td>
  86. </Tr>
  87. <Tr>
  88. <Td>Score</Td>
  89. <Td>{(data.top_score * 100).toFixed(2)}%</Td>
  90. </Tr>
  91. <Tr index={1}>
  92. <Td>Zones</Td>
  93. <Td>{data.zones.join(', ')}</Td>
  94. </Tr>
  95. </Tbody>
  96. </Table>
  97. {data.has_clip ? (
  98. <Fragment>
  99. <Heading size="lg">Clip</Heading>
  100. <VideoPlayer
  101. options={{
  102. sources: [
  103. {
  104. src: `${apiHost}/clips/${data.camera}-${eventId}.mp4`,
  105. type: 'video/mp4',
  106. },
  107. ],
  108. poster: data.has_snapshot
  109. ? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
  110. : `data:image/jpeg;base64,${data.thumbnail}`,
  111. }}
  112. seekOptions={{ forward: 10, back: 5 }}
  113. onReady={(player) => {}}
  114. />
  115. <div className="text-center">
  116. <Button className="mx-2" color="blue" href={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} download>
  117. <Clip className="w-6" /> Download Clip
  118. </Button>
  119. <Button className="mx-2" color="blue" href={`${apiHost}/clips/${data.camera}-${eventId}.jpg`} download>
  120. <Snapshot className="w-6" /> Download Snapshot
  121. </Button>
  122. </div>
  123. </Fragment>
  124. ) : (
  125. <Fragment>
  126. <Heading size="sm">{data.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
  127. <img
  128. src={
  129. data.has_snapshot
  130. ? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
  131. : `data:image/jpeg;base64,${data.thumbnail}`
  132. }
  133. alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
  134. />
  135. </Fragment>
  136. )}
  137. </div>
  138. );
  139. }