diff --git a/.gitignore b/.gitignore index 33ec9ee24..a0f62b7eb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ config/* !config/*.example models *.mp4 -*.ts *.db *.csv frigate/version.py diff --git a/web-new/src/api/baseUrl.ts b/web-new/src/api/baseUrl.ts new file mode 100644 index 000000000..a47355e97 --- /dev/null +++ b/web-new/src/api/baseUrl.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + baseUrl?: any; + } + } + +export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`; \ No newline at end of file diff --git a/web-new/src/lib/formatTimeAgo.ts b/web-new/src/lib/formatTimeAgo.ts new file mode 100644 index 000000000..ee77bd3e0 --- /dev/null +++ b/web-new/src/lib/formatTimeAgo.ts @@ -0,0 +1,25 @@ +const formatter = new Intl.RelativeTimeFormat(undefined, { + numeric: "always", + }) + + const DIVISIONS: { amount: number; name: Intl.RelativeTimeFormatUnit }[] = [ + { amount: 60, name: "seconds" }, + { amount: 60, name: "minutes" }, + { amount: 24, name: "hours" }, + { amount: 7, name: "days" }, + { amount: 4.34524, name: "weeks" }, + { amount: 12, name: "months" }, + { amount: Number.POSITIVE_INFINITY, name: "years" }, + ] + + export function formatTimeAgo(date: Date) { + let duration = (date.getTime() - new Date().getTime()) / 1000 + + for (let i = 0; i < DIVISIONS.length; i++) { + const division = DIVISIONS[i] + if (Math.abs(duration) < division.amount) { + return formatter.format(Math.round(duration), division.name) + } + duration /= division.amount + } + } \ No newline at end of file diff --git a/web-new/src/lib/utils.ts b/web-new/src/lib/utils.ts new file mode 100644 index 000000000..d084ccade --- /dev/null +++ b/web-new/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/web-new/src/pages/Dashboard.tsx b/web-new/src/pages/Dashboard.tsx index 93e0e0a49..466bd8990 100644 --- a/web-new/src/pages/Dashboard.tsx +++ b/web-new/src/pages/Dashboard.tsx @@ -12,7 +12,7 @@ import { } from "@/components/ui/select"; import { useDetectState } from "@/api/ws"; import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig.ts"; +import { FrigateConfig } from "@/types/frigateConfig"; import Heading from "@/components/ui/heading"; export function Dashboard() { diff --git a/web-new/src/types/frigateConfig.ts b/web-new/src/types/frigateConfig.ts new file mode 100644 index 000000000..44afd96dc --- /dev/null +++ b/web-new/src/types/frigateConfig.ts @@ -0,0 +1,402 @@ +export interface FrigateConfig { + audio: { + enabled: boolean; + enabled_in_config: boolean | null; + filters: string[] | null; + listen: string[]; + max_not_heard: number; + min_volume: number; + num_threads: number; + }; + + birdseye: { + enabled: boolean; + height: number; + mode: "objects"; + quality: number; + restream: boolean; + width: number; + }; + + cameras: { + [cameraName: string]: { + audio: { + enabled: boolean; + enabled_in_config: boolean; + filters: string[] | null; + listen: string[]; + max_not_heard: number; + min_volume: number; + num_threads: number; + }; + best_image_timeout: number; + birdseye: { + enabled: boolean; + mode: "objects"; + order: number; + }; + detect: { + annotation_offset: number; + enabled: boolean; + fps: number; + height: number; + max_disappeared: number; + min_initialized: number; + stationary: { + interval: number; + max_frames: { + default: number | null; + objects: Record; + }; + threshold: number; + }; + width: number; + }; + enabled: boolean; + ffmpeg: { + global_args: string[]; + hwaccel_args: string; + input_args: string; + inputs: { + global_args: string[]; + hwaccel_args: string[]; + input_args: string; + path: string; + roles: string[]; + }[]; + output_args: { + detect: string[]; + record: string; + rtmp: string; + }; + retry_interval: number; + }; + ffmpeg_cmds: { + cmd: string; + roles: string[]; + }[]; + live: { + height: number; + quality: number; + stream_name: string; + }; + motion: { + contour_area: number; + delta_alpha: number; + frame_alpha: number; + frame_height: number; + improve_contrast: boolean; + lightning_threshold: number; + mask: string[]; + mqtt_off_delay: number; + threshold: number; + }; + mqtt: { + bounding_box: boolean; + crop: boolean; + enabled: boolean; + height: number; + quality: number; + required_zones: string[]; + timestamp: boolean; + }; + name: string; + objects: { + filters: { + [objectName: string]: { + mask: string | null; + max_area: number; + max_ratio: number; + min_area: number; + min_ratio: number; + min_score: number; + threshold: number; + }; + }; + mask: string; + track: string[]; + }; + onvif: { + autotracking: { + calibrate_on_startup: boolean, + enabled: boolean; + enabled_in_config: boolean; + movement_weights: string[]; + required_zones: string[]; + return_preset: string; + timeout: number; + track: string[]; + zoom_factor: number; + zooming: string; + }; + host: string; + password: string | null; + port: number; + user: string | null; + }; + record: { + enabled: boolean; + enabled_in_config: boolean; + events: { + objects: string[] | null; + post_capture: number; + pre_capture: number; + required_zones: string[]; + retain: { + default: number; + mode: string; + objects: Record; + }; + }; + expire_interval: number; + export: { + timelapse_args: string; + }; + preview: { + quality: string; + }; + retain: { + days: number; + mode: string; + }; + sync_recordings: boolean; + }; + rtmp: { + enabled: boolean; + }; + snapshots: { + bounding_box: boolean; + clean_copy: boolean; + crop: boolean; + enabled: boolean; + height: number | null; + quality: number; + required_zones: string[]; + retain: { + default: number; + mode: string; + objects: Record; + }; + timestamp: boolean; + }; + timestamp_style: { + color: { + blue: number; + green: number; + red: number; + }; + effect: string | null; + format: string; + position: string; + thickness: number; + }; + ui: { + dashboard: boolean; + order: number; + }; + webui_url: string | null; + zones: { + [zoneName: string]: { + coordinates: string; + filters: Record; + inertia: number; + objects: any[]; + }; + }; + }; + }; + + database: { + path: string; + }; + + detect: { + annotation_offset: number; + enabled: boolean; + fps: number; + height: number | null; + max_disappeared: number | null; + min_initialized: number | null; + stationary: { + interval: number | null; + max_frames: { + default: number | null; + objects: Record; + }; + threshold: number | null; + }; + width: number | null; + }; + + detectors: { + coral: { + device: string; + model: { + height: number; + input_pixel_format: string; + input_tensor: string; + labelmap: Record; + labelmap_path: string | null; + model_type: string; + path: string; + width: number; + }; + type: string; + }; + }; + + environment_vars: Record; + + ffmpeg: { + global_args: string[]; + hwaccel_args: string; + input_args: string; + output_args: { + detect: string[]; + record: string; + rtmp: string; + }; + retry_interval: number; + }; + + go2rtc: Record; + + live: { + height: number; + quality: number; + stream_name: string; + }; + + logger: { + default: string; + logs: Record; + }; + + model: { + height: number; + input_pixel_format: string; + input_tensor: string; + labelmap: Record; + labelmap_path: string | null; + model_type: string; + path: string | null; + width: number; + }; + + motion: Record | null; + + mqtt: { + client_id: string; + enabled: boolean; + host: string; + port: number; + stats_interval: number; + tls_ca_certs: string | null; + tls_client_cert: string | null; + tls_client_key: string | null; + tls_insecure: boolean | null; + topic_prefix: string; + user: string | null; + }; + + objects: { + filters: { + [objectName: string]: { + mask: string | null; + max_area: number; + max_ratio: number; + min_area: number; + min_ratio: number; + min_score: number; + threshold: number; + }; + }; + mask: string; + track: string[]; + }; + + plus: { + enabled: boolean; + }; + + record: { + enabled: boolean; + enabled_in_config: boolean | null; + events: { + objects: string[] | null; + post_capture: number; + pre_capture: number; + required_zones: string[]; + retain: { + default: number; + mode: string; + objects: Record; + }; + }; + expire_interval: number; + export: { + timelapse_args: string; + }; + preview: { + quality: string; + }; + retain: { + days: number; + mode: string; + }; + sync_recordings: boolean; + }; + + rtmp: { + enabled: boolean; + }; + + snapshots: { + bounding_box: boolean; + clean_copy: boolean; + crop: boolean; + enabled: boolean; + height: number | null; + quality: number; + required_zones: string[]; + retain: { + default: number; + mode: string; + objects: Record; + }; + timestamp: boolean; + }; + + telemetry: { + network_interfaces: any[]; + stats: { + amd_gpu_stats: boolean; + intel_gpu_stats: boolean; + network_bandwidth: boolean; + }; + version_check: boolean; + }; + + timestamp_style: { + color: { + blue: number; + green: number; + red: number; + }; + effect: string | null; + format: string; + position: string; + thickness: number; + }; + + ui: { + date_style: string; + live_mode: string; + strftime_fmt: string | null; + time_format: string; + time_style: string; + timezone: string | null; + use_experimental: boolean; + }; + +} \ No newline at end of file diff --git a/web-new/src/vite-env.d.ts b/web-new/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/web-new/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web-new/vite.config.ts b/web-new/vite.config.ts new file mode 100644 index 000000000..147bff22d --- /dev/null +++ b/web-new/vite.config.ts @@ -0,0 +1,61 @@ +/// +import path from "path" +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' +import monacoEditorPlugin from 'vite-plugin-monaco-editor'; + +// https://vitejs.dev/config/ +export default defineConfig({ + define: { + 'import.meta.vitest': 'undefined', + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:5000', + ws: true, + }, + '/vod': { + target: 'http://localhost:5000' + }, + '/exports': { + target: 'http://localhost:5000' + }, + '/ws': { + target: 'ws://localhost:5000', + ws: true, + }, + '/live': { + target: 'ws://localhost:5000', + changeOrigin: true, + ws: true, + }, + } + }, + plugins: [ + react(), + monacoEditorPlugin.default({ + customWorkers: [{ label: 'yaml', entry: 'monaco-yaml/yaml.worker' }], + languageWorkers: ['editorWorkerService'], // we don't use any of the default languages + }), + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + test: { + environment: 'jsdom', + alias: { + 'testing-library': path.resolve(__dirname, './__test__/testing-library.js'), + }, + setupFiles: ['./__test__/test-setup.ts'], + includeSource: ['src/**/*.{js,jsx,ts,tsx}'], + coverage: { + reporter: ['text-summary', 'text'], + }, + mockReset: true, + restoreMocks: true, + globals: true, + }, +})