mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-05 13:07:44 +03:00
feat: add more i18n keys
This commit is contained in:
parent
9e6272d065
commit
94d0c66402
@ -36,6 +36,8 @@
|
||||
"second": "{{time}} seconds",
|
||||
"formattedTimestamp": "%b %-d, %I:%M:%S %p",
|
||||
"formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
|
||||
"formattedTimestamp2": "%m/%d %I:%M:%S%P",
|
||||
"formattedTimestamp2.24hour": "%d %b %H:%M:%S",
|
||||
"formattedTimestampExcludeSeconds": "%b %-d, %I:%M %p",
|
||||
"formattedTimestampExcludeSeconds.24hour": "%b %-d, %H:%M",
|
||||
"formattedTimestampWithYear": "%b %-d %Y, %I:%M %p",
|
||||
@ -48,6 +50,9 @@
|
||||
"kph": "kph"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"back": "Go back"
|
||||
},
|
||||
"button": {
|
||||
"apply": "Apply",
|
||||
"reset": "Reset",
|
||||
@ -75,7 +80,11 @@
|
||||
"download": "Download",
|
||||
"info": "Info",
|
||||
"suspended": "Suspended",
|
||||
"unsuspended": "Unsuspend"
|
||||
"unsuspended": "Unsuspend",
|
||||
"play": "Play",
|
||||
"unselect": "Unselect",
|
||||
"export": "Export",
|
||||
"deleteNow": "Delete Now"
|
||||
},
|
||||
"menu": {
|
||||
"system": "System",
|
||||
@ -87,13 +96,15 @@
|
||||
"languages": "Languages",
|
||||
"language": {
|
||||
"en": "English",
|
||||
"zhCN": "简体中文(Simplified Chinese)"
|
||||
"zhCN": "简体中文(Simplified Chinese)",
|
||||
"withSystem.label": "Use the system settings for languag"
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"darkMode": {
|
||||
"label": "Dark Mode",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
"dark": "Dark",
|
||||
"withSystem.label": "Use the system settings for light or dark mode"
|
||||
},
|
||||
"withSystem": "System",
|
||||
"theme": {
|
||||
@ -136,5 +147,13 @@
|
||||
"admin": "Admin",
|
||||
"viewer": "Viewer",
|
||||
"desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI."
|
||||
},
|
||||
"pagination": {
|
||||
"label": "pagination",
|
||||
"previous": "Previous",
|
||||
"previous.label": "Go to previous page",
|
||||
"next": "Next",
|
||||
"next.label": "Go to next page",
|
||||
"more": "More pages"
|
||||
}
|
||||
}
|
||||
|
||||
15
web/public/locales/en/components/auth.json
Normal file
15
web/public/locales/en/components/auth.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"form": {
|
||||
"user": "Username",
|
||||
"password": "Password",
|
||||
"login": "Login",
|
||||
"errors": {
|
||||
"usernameRequired": "Username is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"rateLimit": "Exceeded rate limit. Try again later.",
|
||||
"loginFailed": "Login failed",
|
||||
"unknownError": "Unknown error. Check logs.",
|
||||
"webUnkownError": "Unknown error. Check console logs."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
"add": "Add camera groups",
|
||||
"edit": "Edit camera groups",
|
||||
"delete": {
|
||||
"label": "Delete Camera Group",
|
||||
"confirm": "Confirm Delete",
|
||||
"confirm.desc": "Are you sure you want to delete the camera group <em>{{name}}</em>?"
|
||||
},
|
||||
@ -25,6 +26,7 @@
|
||||
"success": "Camera group ({{name}}) has been saved.",
|
||||
"camera": {
|
||||
"setting": {
|
||||
"label": "Camera Streaming Settings",
|
||||
"title": "{{cameraName}} Streaming Settings",
|
||||
"desc": "Change the live streaming options for this camera group's dashboard. <em>These settings are device/browser-specific.</em>",
|
||||
"audioIsAvailable": "Audio is available for this stream",
|
||||
@ -57,5 +59,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
"label": "Settings",
|
||||
"title": "Options",
|
||||
"showOptions": "Show Options",
|
||||
"hideOptions": "Hide Options"
|
||||
},
|
||||
"boundingBox": "Bounding Box",
|
||||
"timestamp": "Timestamp",
|
||||
"zones": "Zones",
|
||||
"mask": "Mask",
|
||||
"motion": "Motion",
|
||||
"regions": "Regions"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,10 +15,12 @@
|
||||
"desc": "Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model."
|
||||
},
|
||||
"review": {
|
||||
"true.label": "Confirm this label for Frigate Plus",
|
||||
"true_one": "This is a {{label}}",
|
||||
"true_other": "This is an {{label}}",
|
||||
"false_one": "This is not a {{label}}",
|
||||
"false_other": "This is not an {{label}}",
|
||||
"false.label": "Do not confirm this label for Frigate Plus",
|
||||
"state.submitted": "Submitted"
|
||||
}
|
||||
},
|
||||
@ -31,13 +33,18 @@
|
||||
"fromTimeline": "Select from Timeline",
|
||||
"lastHour_one": "Last Hour",
|
||||
"lastHour_other": "Last {{count}} Hours",
|
||||
"custom": "Custom"
|
||||
"custom": "Custom",
|
||||
"start": "Start Time",
|
||||
"start.label": "Select Start Time",
|
||||
"end": "End Time",
|
||||
"end.label": "Select End Time"
|
||||
},
|
||||
"name": {
|
||||
"placeholder": "Name the Export"
|
||||
},
|
||||
"select": "Select",
|
||||
"export": "Export",
|
||||
"selectOrExport": "Select or Export",
|
||||
"toast": {
|
||||
"success": "Successfully started export. View the file in the /exports folder.",
|
||||
"error": {
|
||||
@ -70,13 +77,15 @@
|
||||
"desc": "Provide a name for this saved search.",
|
||||
"placeholder": "Enter a name for your search",
|
||||
"overwrite": "{{searchName}} already exists. Saving will overwrite the existing value.",
|
||||
"success": "Search ({{searchName}}) has been saved."
|
||||
"success": "Search ({{searchName}}) has been saved.",
|
||||
"button.save.label": "Save this search"
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"confirmDelete": {
|
||||
"title": "Confirm Delete",
|
||||
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
|
||||
"desc": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future.",
|
||||
"desc.selected": "Are you sure you want to delete all recorded video associated with this review item?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future."
|
||||
},
|
||||
"button": {
|
||||
"export": "Export",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"filter": "Filter",
|
||||
"labels": {
|
||||
"label": "Labels",
|
||||
"all": "All Labels",
|
||||
"all.short": "Labels",
|
||||
"count": "{{count}} Labels"
|
||||
@ -14,6 +15,7 @@
|
||||
"all.short": "Dates"
|
||||
},
|
||||
"more": "More Filters",
|
||||
"reset.label": "Reset filters to default values",
|
||||
"timeRange": "Time Range",
|
||||
"zones.label": "Zones",
|
||||
"subLabels": {
|
||||
@ -42,12 +44,16 @@
|
||||
"relevance": "Relevance"
|
||||
},
|
||||
"cameras": {
|
||||
"label": "Cameras Filter",
|
||||
"all": "All Cameras",
|
||||
"all.short": "Cameras"
|
||||
},
|
||||
"review": {
|
||||
"showReviewed": "Show Reviewed"
|
||||
},
|
||||
"motion": {
|
||||
"showMotionOnly": "Show Motion Only"
|
||||
},
|
||||
"explore": {
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
@ -65,6 +71,30 @@
|
||||
"description": "Description"
|
||||
}
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"selectDateBy": {
|
||||
"label": "Select a date to filter by"
|
||||
}
|
||||
}
|
||||
},
|
||||
"logSettings": {
|
||||
"label": "Filter log level",
|
||||
"filterBySeverity": "Filter logs by severity",
|
||||
"loading": "Loading",
|
||||
"loading.desc": "When the log pane is scrolled to the bottom, new logs automatically stream as they are added.",
|
||||
"disableLogStreaming": "Disable log streaming",
|
||||
"allLogs": "All logs"
|
||||
},
|
||||
"trackedObjectDelete": {
|
||||
"title": "Confirm Delete",
|
||||
"desc": "Deleting these {{objectLength}} tracked objects removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of these tracked objects in History view will <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?<br /><br />Hold the <em>Shift</em> key to bypass this dialog in the future.",
|
||||
"toast": {
|
||||
"success": "Tracked objects deleted successfully.",
|
||||
"error": "Failed to delete tracked objects: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"zoneMask": {
|
||||
"filterBy": "Filter by zone mask"
|
||||
}
|
||||
}
|
||||
|
||||
10
web/public/locales/en/components/input.json
Normal file
10
web/public/locales/en/components/input.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"button": {
|
||||
"downloadVideo": {
|
||||
"label": "Download Video",
|
||||
"toast": {
|
||||
"success": "Your review item video has started downloading."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
{
|
||||
"noRecordingsFoundForThisTime": "No recordings found for this time",
|
||||
"noPreviewFound": "No Preview Found",
|
||||
"noPreviewFoundFor": "No Preview Found for {{cameraName}}"
|
||||
"noPreviewFoundFor": "No Preview Found for {{cameraName}}",
|
||||
"submitFrigatePlus": {
|
||||
"title": "Submit this frame to Frigate+?",
|
||||
"submit": "Submit"
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,10 @@
|
||||
"motion": "No motion data found"
|
||||
},
|
||||
"timeline": "Timeline",
|
||||
"timeline.aria": "Select timeline",
|
||||
"events": {
|
||||
"label": "Events",
|
||||
"aria": "Select events",
|
||||
"noFoundForTimePeriod": "No events found for this time period."
|
||||
},
|
||||
"documentTitle": "Review - Frigate",
|
||||
@ -22,5 +24,12 @@
|
||||
},
|
||||
"calendarFilter": {
|
||||
"last24Hours": "Last 24 Hours"
|
||||
}
|
||||
},
|
||||
"markAsReviewed": "Mark as Reviewed",
|
||||
"markTheseItemsAsReviewed": "Mark these items as reviewed",
|
||||
"newReviewItems": {
|
||||
"label": "View new review items",
|
||||
"button": "New Items To Review"
|
||||
},
|
||||
"camera": "Camera"
|
||||
}
|
||||
|
||||
@ -34,6 +34,43 @@
|
||||
"video": "video",
|
||||
"object_lifecycle": "object lifecycle"
|
||||
},
|
||||
"objectLifecycle": {
|
||||
"title": "Object Lifecycle",
|
||||
"noImageFound": "No image found for this timestamp.",
|
||||
"createObjectMask": "Create Object Mask",
|
||||
"adjustAnnotationSettings": "Adjust annotation settings",
|
||||
"scrollViewTips": "Scroll to view the significant moments of this object's lifecycle.",
|
||||
"autoTrackingTips": "Bounding box positions will be inaccurate for autotracking cameras.",
|
||||
"lifecycleItemDesc": {
|
||||
"visible": "{{label}} detected",
|
||||
"entered_zone": "{{label}} entered {{zones}}",
|
||||
"active": "{{label}} became active",
|
||||
"stationary": "{{label}} became stationary",
|
||||
"attribute": {
|
||||
"faceOrLicense_plate": "{{attribute}} detected for {{label}}",
|
||||
"other": "{{label}} recognized as {{attribute}}"
|
||||
},
|
||||
"gone": "{{label}} left",
|
||||
"heard": "{{label}} heard",
|
||||
"external": "{{label}} detected"
|
||||
},
|
||||
"annotationSettings": {
|
||||
"title": "Annotation Settings",
|
||||
"showAllZones": "Show All Zones",
|
||||
"showAllZones.desc": "Always show zones on frames where objects have entered a zone.",
|
||||
"offset": {
|
||||
"label": "Annotation Offset",
|
||||
"desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. However, the <code>annotation_offset</code> field can be used to adjust this.",
|
||||
"documentation": "Read the documentation ",
|
||||
"millisecondsToOffset": "Milliseconds to offset detect annotations by. <em>Default: 0</em>",
|
||||
"tips": "TIP: Imagine there is an event clip with a person walking from left to right. If the event timeline bounding box is consistently to the left of the person then the value should be decreased. Similarly, if a person is walking from left to right and the bounding box is consistently ahead of the person then the value should be increased."
|
||||
}
|
||||
},
|
||||
"carousel": {
|
||||
"previous": "Previous slide",
|
||||
"next": "Next slide"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"item": {
|
||||
"title": "Review Item Details",
|
||||
@ -68,6 +105,8 @@
|
||||
"aiTips": "Frigate will not request a description from your Generative AI provider until the tracked object's lifecycle has ended."
|
||||
},
|
||||
"button.regenerate": "Regenerate",
|
||||
"button.regenerate.label": "Regenerate tracked object description",
|
||||
"expandRegenerationMenu": "Expand regeneration menu",
|
||||
"regenerateFromSnapshot": "Regenerate from Snapshot",
|
||||
"regenerateFromThumbnails": "Regenerate from Thumbnails",
|
||||
"tips": {
|
||||
@ -99,6 +138,9 @@
|
||||
"viewInHistory": {
|
||||
"label": "View in History",
|
||||
"aria": "View in History"
|
||||
},
|
||||
"deleteTrackedObject": {
|
||||
"label": "Delete this tracked object"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@ -3,5 +3,11 @@
|
||||
"search": "Search",
|
||||
"noExports": "No exports found",
|
||||
"deleteExport": "Delete Export",
|
||||
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
|
||||
"deleteExport.desc": "Are you sure you want to delete {{exportName}}?",
|
||||
"editExport": {
|
||||
"title": "Rename Export",
|
||||
"desc": "Enter a new name for this export.",
|
||||
"saveExport": "Save Export"
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,6 +12,11 @@
|
||||
},
|
||||
"ptz": {
|
||||
"move": {
|
||||
"clickMove": {
|
||||
"label": "Click in the frame to center the camera",
|
||||
"enable": "Enable click to move",
|
||||
"disable": "Disable click to move"
|
||||
},
|
||||
"left": {
|
||||
"label": "Move PTZ camera to the left"
|
||||
},
|
||||
@ -37,7 +42,8 @@
|
||||
"center": {
|
||||
"label": "Click in the frame to center the PTZ camera"
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": "PTZ camera presets"
|
||||
},
|
||||
"camera": {
|
||||
"enable": "Enable Camera",
|
||||
@ -118,8 +124,7 @@
|
||||
"playInBackground": {
|
||||
"label": "Play in background",
|
||||
"tips": "Enable this option to continue streaming when the player is hidden."
|
||||
},
|
||||
"": ""
|
||||
}
|
||||
},
|
||||
"cameraSettings": {
|
||||
"title": "{{camera}} Settings",
|
||||
@ -129,5 +134,16 @@
|
||||
"snapshots": "Snapshots",
|
||||
"audioDetection": "Audio Detection",
|
||||
"autotracking": "Autotracking"
|
||||
},
|
||||
"history": {
|
||||
"label": "Show historical footage"
|
||||
},
|
||||
"effectiveRetainMode": {
|
||||
"modes": {
|
||||
"all": "All",
|
||||
"motion": "Motion",
|
||||
"active_objects": "Active Objects"
|
||||
},
|
||||
"notAllTips": "Your {{source}} recording retention configuration is set to <code>mode: {{effectiveRetainMode}}</code>, so this on-demand recording will only keep segments with {{effectiveRetainModeName}}."
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"export": "Export",
|
||||
"calendar": "Calendar",
|
||||
"filter": "Filter",
|
||||
"filters": "Filters",
|
||||
"toast": {
|
||||
"error": {
|
||||
"noValidTimeSelected": "No valid time range selected",
|
||||
|
||||
@ -122,6 +122,17 @@
|
||||
"inertia.error.mustBeAboveZero": "Inertia must be above 0.",
|
||||
"loiteringTime.error.mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0.",
|
||||
"polygonDrawing": {
|
||||
"removeLastPoint": "Remove last point",
|
||||
"reset.label": "Clear all points",
|
||||
"snapPoints": {
|
||||
"true": "Snap points",
|
||||
"false": "Don't Snap points"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Confirm Delete",
|
||||
"desc": "Are you sure you want to delete the {{type}} <em>{{name}}</em>?",
|
||||
"success": "{{name}} has been deleted."
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Polygon drawing must be finished before saving."
|
||||
}
|
||||
|
||||
@ -1,7 +1,23 @@
|
||||
{
|
||||
"title": "System",
|
||||
"metrics": "System metrics",
|
||||
"logs": "System logs",
|
||||
"logs": {
|
||||
"download": {
|
||||
"label": "Download Logs"
|
||||
},
|
||||
"copy": {
|
||||
"label": "Copy to Clipboard",
|
||||
"success": "Copied logs to clipboard",
|
||||
"error": "Could not copy logs to clipboard"
|
||||
},
|
||||
"type": {
|
||||
"label": "Type",
|
||||
"timestamp": "Timestamp",
|
||||
"tag": "Tag",
|
||||
"message": "Message"
|
||||
},
|
||||
"tips": "Logs are streaming from the server"
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"detector": {
|
||||
@ -15,7 +31,24 @@
|
||||
"gpuUsage": "GPU Usage",
|
||||
"gpuMemory": "GPU Memory",
|
||||
"gpuEncoder": "GPU Encoder",
|
||||
"gpuDecoder": "GPU Decoder"
|
||||
"gpuDecoder": "GPU Decoder",
|
||||
"gpuInfo": {
|
||||
"vainfoOutput": {
|
||||
"title": "Vainfo Output",
|
||||
"returnCode": "Return Code: {{code}}",
|
||||
"processOutput": "Process Output:",
|
||||
"processError": "Process Error:"
|
||||
},
|
||||
"nvidiaSMIOutput": {
|
||||
"title": "Nvidia SMI Output",
|
||||
"name": "Name: {{name}}",
|
||||
"driver": "Driver: {{driver}}",
|
||||
"cudaComputerCapability": "CUDA Compute Capability: {{cuda_compute}}",
|
||||
"vbios": "VBios Info: {{vbios}}"
|
||||
},
|
||||
"closeInfo.label": "Close GPU info",
|
||||
"copyInfo.label": "Close GPU info"
|
||||
}
|
||||
},
|
||||
"otherProcesses": {
|
||||
"title": "Other Processes",
|
||||
@ -28,12 +61,14 @@
|
||||
"overview": "Overview",
|
||||
"recordings": {
|
||||
"title": "Recordings",
|
||||
"tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk."
|
||||
"tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.",
|
||||
"earliestRecording": "Earliest recording available:"
|
||||
},
|
||||
"cameraStorage": {
|
||||
"title": "Camera Storage",
|
||||
"camera": "Camera",
|
||||
"unused": "Unused",
|
||||
"unusedStorageInformation": "Unused Storage Information",
|
||||
"storageUsed": "Storage Used",
|
||||
"percentageOfTotalUsed": "Percentage of Total Used",
|
||||
"bandwidth": "Bandwidth",
|
||||
|
||||
@ -48,6 +48,14 @@
|
||||
"kph": "英里/小时"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"label": "分页",
|
||||
"previous": "上一页",
|
||||
"previous.label": "转到上一页",
|
||||
"next": "下一页",
|
||||
"next.label": "转到下一页",
|
||||
"more": "更多页面"
|
||||
},
|
||||
"button": {
|
||||
"apply": "应用",
|
||||
"reset": "重置",
|
||||
@ -75,7 +83,11 @@
|
||||
"download": "下载",
|
||||
"info": "信息",
|
||||
"suspended": "已暂停",
|
||||
"unsuspended": "取消暂停"
|
||||
"unsuspended": "取消暂停",
|
||||
"play": "播放",
|
||||
"unselect": "取消选择",
|
||||
"export": "导出",
|
||||
"deleteNow": "立即删除"
|
||||
},
|
||||
"menu": {
|
||||
"system": "系统",
|
||||
@ -87,13 +99,15 @@
|
||||
"languages": "languages / 语言",
|
||||
"language": {
|
||||
"en": "English",
|
||||
"zhCN": "简体中文"
|
||||
"zhCN": "简体中文",
|
||||
"withSystem.label": "使用系统语言设置"
|
||||
},
|
||||
"appearance": "外观",
|
||||
"darkMode": {
|
||||
"label": "深色模式",
|
||||
"light": "浅色",
|
||||
"dark": "深色"
|
||||
"dark": "深色",
|
||||
"withSystem.label": "使用系统深色模式设置"
|
||||
},
|
||||
"withSystem": "跟随系统",
|
||||
"theme": {
|
||||
|
||||
15
web/public/locales/zh-CN/components/auth.json
Normal file
15
web/public/locales/zh-CN/components/auth.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"form": {
|
||||
"user": "用户名",
|
||||
"password": "密码",
|
||||
"login": "登录",
|
||||
"errors": {
|
||||
"usernameRequired": "用户名不能为空",
|
||||
"passwordRequired": "密码不能为空",
|
||||
"rateLimit": "超出请求限制,请稍后再试。",
|
||||
"loginFailed": "登录失败",
|
||||
"unknownError": "未知错误,请检查日志。",
|
||||
"webUnkownError": "未知错误,请检查控制台日志。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
"add": "添加摄像头组",
|
||||
"edit": "编辑摄像头组",
|
||||
"delete": {
|
||||
"label": "删除摄像头组",
|
||||
"confirm": "确认删除",
|
||||
"confirm.desc": "你确定要删除摄像头组 <em>{{name}}</em> 吗?"
|
||||
},
|
||||
@ -25,6 +26,7 @@
|
||||
"success": "摄像头组({{name}})保存成功。",
|
||||
"camera": {
|
||||
"setting": {
|
||||
"label": "摄像头视频流设置",
|
||||
"title": "{{cameraName}} 视频流设置",
|
||||
"desc": "更改此摄像头组仪表板的实时视频流选项。<em>这些设置特定于设备/浏览器。</em>",
|
||||
"audioIsAvailable": "此视频流支持音频",
|
||||
@ -57,5 +59,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
"label": "设置",
|
||||
"title": "选项",
|
||||
"showOptions": "显示选项",
|
||||
"hideOptions": "隐藏选项"
|
||||
},
|
||||
"boundingBox": "边界框",
|
||||
"timestamp": "时间戳",
|
||||
"zones": "区域",
|
||||
"mask": "遮罩",
|
||||
"motion": "运动",
|
||||
"regions": "区域"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,10 @@
|
||||
"desc": "您希望避开的地点中的物体不应被视为误报。若将其作为误报提交,可能会导致AI模型容易混淆相关物体的识别。"
|
||||
},
|
||||
"review": {
|
||||
"true.label": "为 Frigate Plus 确认此标签",
|
||||
"true_one": "这是 {{label}}",
|
||||
"true_other": "这是 {{label}}",
|
||||
"false.label": "不为 Frigate Plus 确认此标签",
|
||||
"false_one": "这不是 {{label}}",
|
||||
"false_other": "这不是 {{label}}",
|
||||
"state.submitted": "已提交"
|
||||
@ -31,13 +33,18 @@
|
||||
"fromTimeline": "从时间线选择",
|
||||
"lastHour_one": "最后1小时",
|
||||
"lastHour_other": "最后 {{count}} 小时",
|
||||
"custom": "自定义"
|
||||
"custom": "自定义",
|
||||
"start": "开始时间",
|
||||
"start.label": "选择开始时间",
|
||||
"end": "结束时间",
|
||||
"end.label": "选择结束时间"
|
||||
},
|
||||
"name": {
|
||||
"placeholder": "导出项目的名字"
|
||||
},
|
||||
"select": "选择",
|
||||
"export": "导出",
|
||||
"selectOrExport": "选择或导出",
|
||||
"toast": {
|
||||
"success": "导出成功。进入 /exports 目录查看文件。",
|
||||
"error": {
|
||||
@ -70,13 +77,15 @@
|
||||
"desc": "请为此已保存的搜索提供一个名称。",
|
||||
"placeholder": "请输入搜索名称",
|
||||
"overwrite": "{{searchName}} 已存在。保存将覆盖现有值。",
|
||||
"success": "搜索 ({{searchName}}) 已保存。"
|
||||
"success": "搜索 ({{searchName}}) 已保存。",
|
||||
"button.save.label": "保存此搜索"
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
"confirmDelete": {
|
||||
"title": "确认删除",
|
||||
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
|
||||
"desc": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。",
|
||||
"desc.selected": "您确定要删除与此审核项相关的所有录制视频吗?<br /><br />提示:按住 <em>Shift</em> 键点击删除可跳过此对话框。"
|
||||
},
|
||||
"button": {
|
||||
"export": "导出",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"filter": "过滤器",
|
||||
"labels": {
|
||||
"label": "标签",
|
||||
"all": "所有标签",
|
||||
"all.short": "标签",
|
||||
"count": "{{count}} 个标签"
|
||||
@ -14,6 +15,7 @@
|
||||
"all.short": "日期"
|
||||
},
|
||||
"more": "更多筛选项",
|
||||
"reset.label": "重置筛选器为默认值",
|
||||
"timeRange": "时间范围",
|
||||
"zones.label": "区域",
|
||||
"subLabels": {
|
||||
@ -42,12 +44,16 @@
|
||||
"relevance": "关联性"
|
||||
},
|
||||
"cameras": {
|
||||
"label": "摄像头筛选",
|
||||
"all": "所有摄像头",
|
||||
"all.short": "摄像头"
|
||||
},
|
||||
"review": {
|
||||
"showReviewed": "显示已查看的项目"
|
||||
},
|
||||
"motion": {
|
||||
"showMotionOnly": "仅显示运动"
|
||||
},
|
||||
"explore": {
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
@ -64,8 +70,31 @@
|
||||
"thumbnailImage": "缩略图",
|
||||
"description": "描述"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"selectDateBy": {
|
||||
"label": "选择日期进行筛选"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"logSettings": {
|
||||
"label": "日志级别筛选",
|
||||
"filterBySeverity": "按严重程度筛选日志",
|
||||
"loading": "加载中",
|
||||
"loading.desc": "当日志面板滚动到底部时,新的日志会自动流式加载。",
|
||||
"disableLogStreaming": "禁用日志流式加载",
|
||||
"allLogs": "所有日志"
|
||||
},
|
||||
"trackedObjectDelete": {
|
||||
"title": "确认删除",
|
||||
"desc": "删除这 {{objectLength}} 个跟踪对象将移除快照、任何已保存的嵌入和任何相关的对象生命周期条目。历史视图中这些跟踪对象的录制片段将<em>不会</em>被删除。<br /><br />您确定要继续吗?<br /><br />按住 <em>Shift</em> 键可在将来跳过此对话框。",
|
||||
"toast": {
|
||||
"success": "跟踪对象删除成功。",
|
||||
"error": "删除跟踪对象失败:{{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"zoneMask": {
|
||||
"filterBy": "按区域遮罩筛选"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
{
|
||||
|
||||
"button": {
|
||||
"downloadVideo": {
|
||||
"label": "下载视频",
|
||||
"toast": {
|
||||
"success": "下载成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
{
|
||||
"noRecordingsFoundForThisTime": "找不到此次录制",
|
||||
"noPreviewFound": "没有找到预览",
|
||||
"noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览"
|
||||
"noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
|
||||
"submitFrigatePlus": {
|
||||
"title": "提交此帧到 Frigate+?",
|
||||
"submit": "提交"
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,10 @@
|
||||
"motion": "还没有运动类数据"
|
||||
},
|
||||
"timeline": "时间线",
|
||||
"timeline.aria": "选择时间线",
|
||||
"events": {
|
||||
"label": "事件",
|
||||
"aria": "选择事件",
|
||||
"noFoundForTimePeriod": "未找到该时间段的事件。"
|
||||
},
|
||||
"documentTitle": "预览 - Frigate",
|
||||
@ -22,5 +24,12 @@
|
||||
},
|
||||
"calendarFilter": {
|
||||
"last24Hours": "过去24小时"
|
||||
}
|
||||
},
|
||||
"markAsReviewed": "标记为已审核",
|
||||
"markTheseItemsAsReviewed": "将这些项目标记为已审核",
|
||||
"newReviewItems": {
|
||||
"label": "查看新的审核项目",
|
||||
"button": "新的待审核项目"
|
||||
},
|
||||
"camera": "摄像头"
|
||||
}
|
||||
|
||||
@ -34,6 +34,43 @@
|
||||
"video": "视频",
|
||||
"object_lifecycle": "对象生命周期"
|
||||
},
|
||||
"objectLifecycle": {
|
||||
"title": "对象生命周期",
|
||||
"noImageFound": "未找到此时间戳的图像。",
|
||||
"createObjectMask": "创建对象遮罩",
|
||||
"adjustAnnotationSettings": "调整标注设置",
|
||||
"scrollViewTips": "滚动查看此对象生命周期的重要时刻。",
|
||||
"autoTrackingTips": "自动跟踪摄像头的边界框位置可能不准确。",
|
||||
"lifecycleItemDesc": {
|
||||
"visible": "检测到 {{label}}",
|
||||
"entered_zone": "{{label}} 进入 {{zones}}",
|
||||
"active": "{{label}} 变为活动状态",
|
||||
"stationary": "{{label}} 变为静止状态",
|
||||
"attribute": {
|
||||
"faceOrLicense_plate": "检测到 {{label}} 的 {{attribute}}",
|
||||
"other": "{{label}} 识别为 {{attribute}}"
|
||||
},
|
||||
"gone": "{{label}} 离开",
|
||||
"heard": "听到 {{label}}",
|
||||
"external": "检测到 {{label}}"
|
||||
},
|
||||
"annotationSettings": {
|
||||
"title": "标注设置",
|
||||
"showAllZones": "显示所有区域",
|
||||
"showAllZones.desc": "在对象进入区域的帧上始终显示区域。",
|
||||
"offset": {
|
||||
"label": "标注偏移",
|
||||
"desc": "这些数据来自摄像头的检测源,但是叠加在录制源的图像上。这两个流不太可能完全同步。因此,边界框和录像不会完全对齐。但是,可以使用 <code>annotation_offset</code> 字段来调整这个问题。",
|
||||
"documentation": "阅读文档(英文) ",
|
||||
"millisecondsToOffset": "检测标注的偏移毫秒数。<em>默认值:0</em>",
|
||||
"tips": "提示:假设有一个人从左向右走的事件片段。如果事件时间线上的边界框始终在人的左侧,则应该减小该值。同样,如果一个人从左向右走,而边界框始终在人的前面,则应该增加该值。"
|
||||
}
|
||||
},
|
||||
"carousel": {
|
||||
"previous": "上一张",
|
||||
"next": "下一张"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"item": {
|
||||
"title": "回放项目详情",
|
||||
@ -68,6 +105,8 @@
|
||||
"aiTips": "在跟踪对象的生命周期结束之前,Frigate 不会向您的生成式 AI 提供商请求描述。"
|
||||
},
|
||||
"button.regenerate": "重新生成",
|
||||
"button.regenerate.label": "重新生成跟踪对象描述",
|
||||
"expandRegenerationMenu": "展开重新生成菜单",
|
||||
"regenerateFromSnapshot": "从快照重新生成",
|
||||
"regenerateFromThumbnails": "从缩略图重新生成",
|
||||
"tips": {
|
||||
@ -99,6 +138,9 @@
|
||||
"viewInHistory": {
|
||||
"label": "在历史记录中查看",
|
||||
"aria": "在历史记录中查看"
|
||||
},
|
||||
"deleteTrackedObject": {
|
||||
"label": "删除此跟踪对象"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@ -3,5 +3,10 @@
|
||||
"search": "搜索",
|
||||
"noExports": "没有找到导出的项目",
|
||||
"deleteExport": "删除导出的项目",
|
||||
"deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
|
||||
"deleteExport.desc": "你确定要删除 {{exportName}} 吗?",
|
||||
"editExport": {
|
||||
"title": "重命名导出",
|
||||
"desc": "为此导出项目输入新名称。",
|
||||
"saveExport": "保存导出"
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,11 @@
|
||||
},
|
||||
"ptz": {
|
||||
"move": {
|
||||
"clickMove": {
|
||||
"label": "点击画面以使摄像头居中",
|
||||
"enable": "启用点击移动",
|
||||
"disable": "禁用点击移动"
|
||||
},
|
||||
"left": {
|
||||
"label": "PTZ摄像头向左移动"
|
||||
},
|
||||
@ -37,7 +42,8 @@
|
||||
"center": {
|
||||
"label": "点击将PTZ摄像头画面居中"
|
||||
}
|
||||
}
|
||||
},
|
||||
"presets": "PTZ摄像头预设"
|
||||
},
|
||||
"camera": {
|
||||
"enable": "开启摄像头",
|
||||
@ -128,5 +134,16 @@
|
||||
"snapshots": "快照",
|
||||
"audioDetection": "音频检测",
|
||||
"autotracking": "自动跟踪"
|
||||
},
|
||||
"history": {
|
||||
"label": "显示历史录像"
|
||||
},
|
||||
"effectiveRetainMode": {
|
||||
"modes": {
|
||||
"all": "全部",
|
||||
"motion": "运动",
|
||||
"active_objects": "活动对象"
|
||||
},
|
||||
"notAllTips": "您的 {{source}} 录制保留配置设置为 <code>mode: {{effectiveRetainMode}}</code>,因此此按需录制将仅保留包含 {{effectiveRetainModeName}} 的片段。"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"export": "导出",
|
||||
"calendar": "日历",
|
||||
"filter": "筛选",
|
||||
"filters": "筛选条件",
|
||||
"toast": {
|
||||
"error": {
|
||||
"noValidTimeSelected": "未选择有效的时间范围",
|
||||
|
||||
@ -1,7 +1,23 @@
|
||||
{
|
||||
"title": "系统",
|
||||
"metrics": "系统指标",
|
||||
"logs": "系统日志",
|
||||
"logs": {
|
||||
"download": {
|
||||
"label": "下载日志"
|
||||
},
|
||||
"copy": {
|
||||
"label": "复制到剪贴板",
|
||||
"success": "已复制日志到剪贴板",
|
||||
"error": "无法复制日志到剪贴板"
|
||||
},
|
||||
"type": {
|
||||
"label": "类型",
|
||||
"timestamp": "时间戳",
|
||||
"tag": "标签",
|
||||
"message": "消息"
|
||||
},
|
||||
"tips": "日志正在从服务器流式传输"
|
||||
},
|
||||
"general": {
|
||||
"title": "常规",
|
||||
"detector": {
|
||||
@ -15,7 +31,24 @@
|
||||
"gpuUsage": "GPU使用率",
|
||||
"gpuMemory": "GPU显存",
|
||||
"gpuEncoder": "GPU编码",
|
||||
"gpuDecoder": "GPU解码"
|
||||
"gpuDecoder": "GPU解码",
|
||||
"gpuInfo": {
|
||||
"vainfoOutput": {
|
||||
"title": "Vainfo 输出",
|
||||
"returnCode": "返回代码:{{code}}",
|
||||
"processOutput": "进程输出:",
|
||||
"processError": "进程错误:"
|
||||
},
|
||||
"nvidiaSMIOutput": {
|
||||
"title": "Nvidia SMI 输出",
|
||||
"name": "名称:{{name}}",
|
||||
"driver": "驱动:{{driver}}",
|
||||
"cudaComputerCapability": "CUDA计算能力:{{cuda_compute}}",
|
||||
"vbios": "VBios信息:{{vbios}}"
|
||||
},
|
||||
"closeInfo.label": "关闭GPU信息",
|
||||
"copyInfo.label": "复制GPU信息"
|
||||
}
|
||||
},
|
||||
"otherProcesses": {
|
||||
"title": "其他进程",
|
||||
@ -28,12 +61,14 @@
|
||||
"overview": "概览",
|
||||
"recordings": {
|
||||
"title": "录制内容",
|
||||
"tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。"
|
||||
"tips": "该值表示 Frigate 数据库中录制内容所使用的总存储空间。Frigate 不会追踪磁盘上所有文件的存储使用情况。",
|
||||
"earliestRecording": "最早的可用录制:"
|
||||
},
|
||||
"cameraStorage": {
|
||||
"title": "摄像头存储",
|
||||
"camera": "摄像头",
|
||||
"unused": "未使用",
|
||||
"unusedStorageInformation": "未使用存储信息",
|
||||
"storageUsed": "存储使用",
|
||||
"percentageOfTotalUsed": "总使用率",
|
||||
"bandwidth": "带宽",
|
||||
|
||||
@ -21,16 +21,18 @@ import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { AuthContext } from "@/context/auth-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
const { t } = useTranslation(["components/auth"]);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const { login } = React.useContext(AuthContext);
|
||||
|
||||
const formSchema = z.object({
|
||||
user: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required"),
|
||||
user: z.string().min(1, t("form.errors.usernameRequired")),
|
||||
password: z.string().min(1, t("form.errors.passwordRequired")),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@ -62,20 +64,20 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const err = error as AxiosError;
|
||||
if (err.response?.status === 429) {
|
||||
toast.error("Exceeded rate limit. Try again later.", {
|
||||
toast.error(t("form.errors.rateLimit"), {
|
||||
position: "top-center",
|
||||
});
|
||||
} else if (err.response?.status === 401) {
|
||||
toast.error("Login failed", {
|
||||
toast.error(t("form.errors.loginFailed"), {
|
||||
position: "top-center",
|
||||
});
|
||||
} else {
|
||||
toast.error("Unknown error. Check logs.", {
|
||||
toast.error(t("form.errors.unknownError"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toast.error("Unknown error. Check console logs.", {
|
||||
toast.error(t("form.errors.webUnkownError"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
@ -92,7 +94,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>User</FormLabel>
|
||||
<FormLabel>{t("form.user")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
@ -107,7 +109,7 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t("form.password")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||
@ -123,10 +125,10 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Login"
|
||||
aria-label={t("form.login")}
|
||||
>
|
||||
{isLoading && <ActivityIndicator className="mr-2 h-4 w-4" />}
|
||||
Login
|
||||
{t("form.login")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -3,6 +3,7 @@ import { toast } from "sonner";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type DownloadVideoButtonProps = {
|
||||
source: string;
|
||||
@ -17,6 +18,7 @@ export function DownloadVideoButton({
|
||||
startTime,
|
||||
className,
|
||||
}: DownloadVideoButtonProps) {
|
||||
const { t } = useTranslation(["components/input"]);
|
||||
const formattedDate = formatUnixTimestampToDateTime(startTime, {
|
||||
strftime_fmt: "%D-%T",
|
||||
time_style: "medium",
|
||||
@ -25,7 +27,7 @@ export function DownloadVideoButton({
|
||||
const filename = `${camera}_${formattedDate}.mp4`;
|
||||
|
||||
const handleDownloadStart = () => {
|
||||
toast.success("Your review item video has started downloading.", {
|
||||
toast.success(t("button.downloadVideo.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
};
|
||||
@ -36,7 +38,7 @@ export function DownloadVideoButton({
|
||||
asChild
|
||||
className="flex items-center gap-2"
|
||||
size="sm"
|
||||
aria-label="Download Video"
|
||||
aria-label={t("button.downloadVideo.label")}
|
||||
>
|
||||
<a href={source} download={filename} onClick={handleDownloadStart}>
|
||||
<FaDownload
|
||||
|
||||
@ -7,6 +7,7 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
import { usePersistence } from "@/hooks/use-persistence";
|
||||
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Options = { [key: string]: boolean };
|
||||
|
||||
@ -21,6 +22,7 @@ export default function DebugCameraImage({
|
||||
className,
|
||||
cameraConfig,
|
||||
}: DebugCameraImageProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [options, setOptions] = usePersistence<Options>(
|
||||
`${cameraConfig?.name}-feed`,
|
||||
@ -59,17 +61,21 @@ export default function DebugCameraImage({
|
||||
onClick={handleToggleSettings}
|
||||
variant="link"
|
||||
size="sm"
|
||||
aria-label="Settings"
|
||||
aria-label={t("debug.options.label")}
|
||||
>
|
||||
<span className="h-5 w-5">
|
||||
<LuSettings />
|
||||
</span>{" "}
|
||||
<span>{showSettings ? "Hide" : "Show"} Options</span>
|
||||
<span>
|
||||
{showSettings
|
||||
? t("debug.options.hideOptions")
|
||||
: t("debug.options.showOptions")}
|
||||
</span>
|
||||
</Button>
|
||||
{showSettings ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Options</CardTitle>
|
||||
<CardTitle>{t("debug.options.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DebugSettings
|
||||
@ -89,6 +95,7 @@ type DebugSettingsProps = {
|
||||
};
|
||||
|
||||
function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
const { t } = useTranslation(["components/camera"]);
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
@ -99,7 +106,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("bbox", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="bbox">Bounding Box</Label>
|
||||
<Label htmlFor="bbox">{t("debug.boundingBox")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
@ -109,7 +116,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("timestamp", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="timestamp">Timestamp</Label>
|
||||
<Label htmlFor="timestamp">{t("debug.timestamp")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
@ -119,7 +126,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("zones", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="zones">Zones</Label>
|
||||
<Label htmlFor="zones">{t("debug.zones")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
@ -129,7 +136,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("mask", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="mask">Mask</Label>
|
||||
<Label htmlFor="mask">{t("debug.mask")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
@ -139,7 +146,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("motion", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="motion">Motion</Label>
|
||||
<Label htmlFor="motion">{t("debug.motion")}</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
@ -149,7 +156,7 @@ function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
|
||||
handleSetOption("regions", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="regions">Regions</Label>
|
||||
<Label htmlFor="regions">{t("debug.regions")}</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ import { Skeleton } from "../ui/skeleton";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type AnimatedEventCardProps = {
|
||||
event: ReviewSegment;
|
||||
@ -29,6 +30,7 @@ export function AnimatedEventCard({
|
||||
selectedGroup,
|
||||
updateEvents,
|
||||
}: AnimatedEventCardProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
const apiHost = useApiHost();
|
||||
|
||||
@ -121,7 +123,7 @@ export function AnimatedEventCard({
|
||||
<Button
|
||||
className="absolute right-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
||||
size="xs"
|
||||
aria-label="Mark as Reviewed"
|
||||
aria-label={t("markAsReviewed")}
|
||||
onClick={async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
updateEvents();
|
||||
@ -130,7 +132,7 @@ export function AnimatedEventCard({
|
||||
<FaCircleCheck className="size-3 text-white" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mark as Reviewed</TooltipContent>
|
||||
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{previews != undefined && (
|
||||
|
||||
@ -20,6 +20,7 @@ import { MdEditSquare } from "react-icons/md";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { shareOrCopy } from "@/utils/browserUtil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ExportProps = {
|
||||
className: string;
|
||||
@ -36,6 +37,7 @@ export default function ExportCard({
|
||||
onRename,
|
||||
onDelete,
|
||||
}: ExportProps) {
|
||||
const { t } = useTranslation(["views/exports"]);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [loading, setLoading] = useState(
|
||||
exportedRecording.thumb_path.length > 0,
|
||||
@ -89,10 +91,8 @@ export default function ExportCard({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Rename Export</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a new name for this export.
|
||||
</DialogDescription>
|
||||
<DialogTitle>{t("editExport.title")}</DialogTitle>
|
||||
<DialogDescription>{t("editExport.desc")}</DialogDescription>
|
||||
{editName && (
|
||||
<>
|
||||
<Input
|
||||
@ -113,13 +113,13 @@ export default function ExportCard({
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
aria-label="Save Export"
|
||||
aria-label={t("editExport.saveExport")}
|
||||
size="sm"
|
||||
variant="select"
|
||||
disabled={(editName?.update?.length ?? 0) == 0}
|
||||
onClick={() => submitRename()}
|
||||
>
|
||||
Save
|
||||
{t("button.save", { ns: "common" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
@ -207,7 +207,7 @@ export default function ExportCard({
|
||||
{!exportedRecording.in_progress && (
|
||||
<Button
|
||||
className="absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 cursor-pointer text-white hover:bg-transparent hover:text-white"
|
||||
aria-label="Play"
|
||||
aria-label={t("button.play", { ns: "common" })}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
onSelect(exportedRecording);
|
||||
|
||||
@ -3,6 +3,7 @@ import { Button } from "../ui/button";
|
||||
import { LuRefreshCcw } from "react-icons/lu";
|
||||
import { MutableRefObject, useMemo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type NewReviewDataProps = {
|
||||
className: string;
|
||||
@ -18,6 +19,7 @@ export default function NewReviewData({
|
||||
itemsToReview,
|
||||
pullLatestData,
|
||||
}: NewReviewDataProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
const hasUpdate = useMemo(() => {
|
||||
if (!reviewItems || !itemsToReview) {
|
||||
return false;
|
||||
@ -36,7 +38,7 @@ export default function NewReviewData({
|
||||
: "invisible",
|
||||
"mx-auto bg-gray-400 text-center text-white",
|
||||
)}
|
||||
aria-label="View new review items"
|
||||
aria-label={t("newReviewItems.label")}
|
||||
onClick={() => {
|
||||
pullLatestData();
|
||||
if (contentRef.current) {
|
||||
@ -48,7 +50,7 @@ export default function NewReviewData({
|
||||
}}
|
||||
>
|
||||
<LuRefreshCcw className="mr-2 h-4 w-4" />
|
||||
New Items To Review
|
||||
{t("newReviewItems.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,6 +15,7 @@ import { DateRange } from "react-day-picker";
|
||||
import { useState } from "react";
|
||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { t } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CalendarFilterButtonProps = {
|
||||
reviewSummary?: ReviewSummary;
|
||||
@ -28,16 +29,17 @@ export default function CalendarFilterButton({
|
||||
day,
|
||||
updateSelectedDay,
|
||||
}: CalendarFilterButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedDate = useFormattedTimestamp(
|
||||
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
||||
t("time.formattedTimestampOnlyMonthAndDay"),
|
||||
t("time.formattedTimestampOnlyMonthAndDay", { ns: "common" }),
|
||||
);
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Select a date to filter by"
|
||||
aria-label={t("date.selectDateBy.label")}
|
||||
variant={day == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
@ -64,7 +66,7 @@ export default function CalendarFilterButton({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={() => {
|
||||
updateSelectedDay(undefined);
|
||||
}}
|
||||
@ -107,7 +109,7 @@ export function CalendarRangeFilterButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Select a date to filter by"
|
||||
aria-label={t("date.selectDateBy.label")}
|
||||
variant={range == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
|
||||
@ -154,7 +154,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary text-secondary-foreground focus:bg-secondary focus:text-secondary-foreground"
|
||||
}
|
||||
aria-label="All Cameras"
|
||||
aria-label={t("menu.live.allCameras", { ns: "common" })}
|
||||
size="xs"
|
||||
onClick={() => (group ? setGroup("default", true) : null)}
|
||||
onMouseEnter={() => (isDesktop ? showTooltip("default") : null)}
|
||||
@ -179,7 +179,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
? "bg-blue-900 bg-opacity-60 text-selected focus:bg-blue-900 focus:bg-opacity-60"
|
||||
: "bg-secondary text-secondary-foreground"
|
||||
}
|
||||
aria-label="Camera Group"
|
||||
aria-label={t("group.label")}
|
||||
size="xs"
|
||||
onClick={() => setGroup(name, group != "default")}
|
||||
onMouseEnter={() => (isDesktop ? showTooltip(name) : null)}
|
||||
@ -206,7 +206,7 @@ export function CameraGroupSelector({ className }: CameraGroupSelectorProps) {
|
||||
|
||||
<Button
|
||||
className="bg-secondary text-muted-foreground"
|
||||
aria-label="Add camera group"
|
||||
aria-label={t("group.add")}
|
||||
size="xs"
|
||||
onClick={() => setAddGroup(true)}
|
||||
>
|
||||
@ -278,9 +278,15 @@ function NewGroupDialog({
|
||||
} else {
|
||||
setOpen(false);
|
||||
setEditState("none");
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -290,7 +296,7 @@ function NewGroupDialog({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to save config changes: ${errorMessage}`, {
|
||||
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
@ -305,6 +311,7 @@ function NewGroupDialog({
|
||||
setOpen,
|
||||
deleteGroup,
|
||||
deleteGridLayout,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
@ -373,7 +380,7 @@ function NewGroupDialog({
|
||||
"size-6 rounded-md bg-secondary-foreground p-1 text-background",
|
||||
isMobile && "text-secondary-foreground",
|
||||
)}
|
||||
aria-label="Add camera group"
|
||||
aria-label={t("group.add")}
|
||||
onClick={() => {
|
||||
setEditState("add");
|
||||
}}
|
||||
@ -407,9 +414,7 @@ function NewGroupDialog({
|
||||
<Title>
|
||||
{editState == "add" ? t("group.add") : t("group.edit")}
|
||||
</Title>
|
||||
<Description className="sr-only">
|
||||
Edit camera groups
|
||||
</Description>
|
||||
<Description className="sr-only">{t("group.edit")}</Description>
|
||||
</Header>
|
||||
<CameraGroupEdit
|
||||
currentGroups={currentGroups}
|
||||
@ -563,13 +568,13 @@ export function CameraGroupRow({
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
aria-label="Edit group"
|
||||
aria-label={t("group.edit")}
|
||||
onClick={onEditGroup}
|
||||
>
|
||||
{t("button.edit", { ns: "common" })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
aria-label="Delete group"
|
||||
aria-label={t("group.delete.label")}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
{t("button.delete", { ns: "common" })}
|
||||
@ -855,7 +860,7 @@ export function CameraGroupEdit({
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex h-auto items-center gap-1"
|
||||
aria-label="Camera streaming settings"
|
||||
aria-label={t("group.camera.setting.label")}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
disabled={
|
||||
@ -934,7 +939,7 @@ export function CameraGroupEdit({
|
||||
<Button
|
||||
type="button"
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
@ -943,7 +948,7 @@ export function CameraGroupEdit({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -59,7 +59,7 @@ export function CamerasFilterButton({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Cameras Filter"
|
||||
aria-label={t("cameras.label")}
|
||||
variant={selectedCameras?.length == undefined ? "default" : "select"}
|
||||
size="sm"
|
||||
>
|
||||
@ -228,7 +228,7 @@ export function CamerasFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
variant="select"
|
||||
disabled={currentCameras?.length === 0}
|
||||
onClick={() => {
|
||||
@ -239,7 +239,7 @@ export function CamerasFilterContent({
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setCurrentCameras(undefined);
|
||||
updateCameraFilter(undefined);
|
||||
|
||||
@ -9,6 +9,8 @@ import { Switch } from "../ui/switch";
|
||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
|
||||
type LogSettingsButtonProps = {
|
||||
selectedLabels?: LogSeverity[];
|
||||
@ -22,23 +24,26 @@ export function LogSettingsButton({
|
||||
logSettings,
|
||||
setLogSettings,
|
||||
}: LogSettingsButtonProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
const trigger = (
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Filter log level"
|
||||
aria-label={t("logSettings.label")}
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">Settings</div>
|
||||
<div className="hidden text-primary md:block">
|
||||
{t("menu.settings", { ns: "common" })}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<div className={cn("my-3 space-y-3 py-3 md:mt-0 md:py-0")}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Filter</div>
|
||||
<div className="text-md">{t("filter")}</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
Filter logs by severity.
|
||||
{t("logSettings.filterBySeverity")}
|
||||
</div>
|
||||
</div>
|
||||
<GeneralFilterContent
|
||||
@ -49,14 +54,13 @@ export function LogSettingsButton({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Loading</div>
|
||||
<div className="text-md">{t("logSettings.loading")}</div>
|
||||
<div className="mt-2.5 flex flex-col gap-2.5">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
When the log pane is scrolled to the bottom, new logs
|
||||
automatically stream as they are added.
|
||||
{t("logSettings.loading.desc")}
|
||||
</div>
|
||||
<FilterSwitch
|
||||
label="Disable log streaming"
|
||||
label={t("logSettings.disableLogStreaming")}
|
||||
isChecked={logSettings?.disableStreaming ?? false}
|
||||
onCheckedChange={(isChecked) => {
|
||||
setLogSettings({
|
||||
@ -105,7 +109,7 @@ export function GeneralFilterContent({
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
>
|
||||
All Logs
|
||||
{t("logSettings.allLogs")}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "../ui/alert-dialog";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type ReviewActionGroupProps = {
|
||||
selectedReviews: string[];
|
||||
@ -29,6 +30,7 @@ export default function ReviewActionGroup({
|
||||
onExport,
|
||||
pullLatestData,
|
||||
}: ReviewActionGroupProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const onClearSelected = useCallback(() => {
|
||||
setSelectedReviews([]);
|
||||
}, [setSelectedReviews]);
|
||||
@ -68,22 +70,24 @@ export default function ReviewActionGroup({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
{t("recording.confirmDelete.title")}
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete all recorded video associated with
|
||||
the selected review items?
|
||||
<br />
|
||||
<br />
|
||||
Hold the <em>Shift</em> key to bypass this dialog in the future.
|
||||
<Trans ns="components/dialog">
|
||||
recording.confirmDelete.desc.selected
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -97,14 +101,14 @@ export default function ReviewActionGroup({
|
||||
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
||||
onClick={onClearSelected}
|
||||
>
|
||||
Unselect
|
||||
{t("button.unselect", { ns: "common" })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
{selectedReviews.length == 1 && (
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Export"
|
||||
aria-label={t("recording.button.export")}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onExport(selectedReviews[0]);
|
||||
@ -112,28 +116,38 @@ export default function ReviewActionGroup({
|
||||
}}
|
||||
>
|
||||
<FaCompactDisc className="text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Export</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("recording.button.export")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Mark as reviewed"
|
||||
aria-label={t("recording.button.markAsReviewed")}
|
||||
size="sm"
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Mark as reviewed</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("recording.button.markAsReviewed")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Delete"
|
||||
aria-label={t("button.delete", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{bypassDialog ? "Delete Now" : "Delete"}
|
||||
{bypassDialog
|
||||
? t("recording.button.deleteNow")
|
||||
: t("button.delete", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -286,7 +286,7 @@ function ShowReviewFilter({
|
||||
|
||||
<Button
|
||||
className="block duration-0 md:hidden"
|
||||
aria-label="Show reviewed"
|
||||
aria-label={t("review.showReviewed")}
|
||||
variant={showReviewedSwitch ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
@ -351,7 +351,7 @@ function GeneralFilterButton({
|
||||
selectedLabels?.length || selectedZones?.length ? "select" : "default"
|
||||
}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Filter"
|
||||
aria-label={t("filter")}
|
||||
>
|
||||
<FaFilter
|
||||
className={`${
|
||||
@ -572,7 +572,7 @@ export function GeneralFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
onApply();
|
||||
@ -581,7 +581,10 @@ export function GeneralFilterContent({
|
||||
>
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button aria-label="Reset" onClick={onReset}>
|
||||
<Button
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={onReset}
|
||||
>
|
||||
{t("button.reset", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
@ -624,7 +627,7 @@ function ShowMotionOnlyButton({
|
||||
<Button
|
||||
size="sm"
|
||||
className="duration-0"
|
||||
aria-label="Show Motion Only"
|
||||
aria-label={t("motion.showMotionOnly", { ns: "components/filter" })}
|
||||
variant={motionOnlyButton ? "select" : "default"}
|
||||
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
|
||||
>
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from "../ui/alert-dialog";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { toast } from "sonner";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type SearchActionGroupProps = {
|
||||
selectedObjects: string[];
|
||||
@ -26,6 +27,7 @@ export default function SearchActionGroup({
|
||||
setSelectedObjects,
|
||||
pullLatestData,
|
||||
}: SearchActionGroupProps) {
|
||||
const { t } = useTranslation(["views/filter"]);
|
||||
const onClearSelected = useCallback(() => {
|
||||
setSelectedObjects([]);
|
||||
}, [setSelectedObjects]);
|
||||
@ -37,7 +39,7 @@ export default function SearchActionGroup({
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.status == 200) {
|
||||
toast.success("Tracked objects deleted successfully.", {
|
||||
toast.success(t("trackedObjectDelete.toast.success"), {
|
||||
position: "top-center",
|
||||
});
|
||||
setSelectedObjects([]);
|
||||
@ -49,11 +51,11 @@ export default function SearchActionGroup({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to delete tracked objects.: ${errorMessage}`, {
|
||||
toast.error(t("trackedObjectDelete.toast.error", { errorMessage }), {
|
||||
position: "top-center",
|
||||
});
|
||||
});
|
||||
}, [selectedObjects, setSelectedObjects, pullLatestData]);
|
||||
}, [selectedObjects, setSelectedObjects, pullLatestData, t]);
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [bypassDialog, setBypassDialog] = useState(false);
|
||||
@ -78,27 +80,27 @@ export default function SearchActionGroup({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
{t("trackedObjectDelete.title")}
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Deleting these {selectedObjects.length} tracked objects removes the
|
||||
snapshot, any saved embeddings, and any associated object lifecycle
|
||||
entries. Recorded footage of these tracked objects in History view
|
||||
will <em>NOT</em> be deleted.
|
||||
<br />
|
||||
<br />
|
||||
Are you sure you want to proceed?
|
||||
<br />
|
||||
<br />
|
||||
Hold the <em>Shift</em> key to bypass this dialog in the future.
|
||||
<Trans
|
||||
ns="components/filter"
|
||||
values={{ objectLength: selectedObjects.length }}
|
||||
>
|
||||
trackedObjectDelete.desc
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={onDelete}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -112,20 +114,22 @@ export default function SearchActionGroup({
|
||||
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
||||
onClick={onClearSelected}
|
||||
>
|
||||
Unselect
|
||||
{t("button.unselect", { ns: "common" })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Delete"
|
||||
aria-label={t("button.delete", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{bypassDialog ? "Delete Now" : "Delete"}
|
||||
{bypassDialog
|
||||
? t("button.deleteNow", { ns: "common" })
|
||||
: t("button.delete", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -253,7 +253,6 @@ function GeneralFilterButton({
|
||||
|
||||
return t("labels.count", {
|
||||
count: selectedLabels.length,
|
||||
ns: "components/filter",
|
||||
});
|
||||
}, [selectedLabels, t]);
|
||||
|
||||
@ -270,7 +269,7 @@ function GeneralFilterButton({
|
||||
size="sm"
|
||||
variant={selectedLabels?.length ? "select" : "default"}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Labels"
|
||||
aria-label={t("labels.label")}
|
||||
>
|
||||
<MdLabel
|
||||
className={`${selectedLabels?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
@ -381,7 +380,7 @@ export function GeneralFilterContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
if (selectedLabels != currentLabels) {
|
||||
@ -394,7 +393,7 @@ export function GeneralFilterContent({
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setCurrentLabels(undefined);
|
||||
updateLabelFilter(undefined);
|
||||
@ -442,7 +441,7 @@ function SortTypeButton({
|
||||
: "default"
|
||||
}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Labels"
|
||||
aria-label={t("labels.label")}
|
||||
>
|
||||
<MdSort
|
||||
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
@ -557,7 +556,7 @@ export function SortTypeContent({
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
if (selectedSortType != currentSortType) {
|
||||
@ -570,7 +569,7 @@ export function SortTypeContent({
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setCurrentSortType(undefined);
|
||||
updateSortType(undefined);
|
||||
|
||||
@ -23,7 +23,7 @@ export function ZoneMaskFilterButton({
|
||||
size="sm"
|
||||
variant={selectedZoneMask?.length ? "select" : "default"}
|
||||
className="flex items-center gap-2 capitalize"
|
||||
aria-label="Filter by zone mask"
|
||||
aria-label={t("zoneMask.filterBy")}
|
||||
>
|
||||
<FaFilter
|
||||
className={`${selectedZoneMask?.length ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
@ -31,7 +31,7 @@ export function ZoneMaskFilterButton({
|
||||
<div
|
||||
className={`hidden md:block ${selectedZoneMask?.length ? "text-selected-foreground" : "text-primary"}`}
|
||||
>
|
||||
{t("label")}
|
||||
{t("filter")}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -205,11 +205,15 @@ export function CombinedStorageGraph({
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
aria-label="Unused Storage Information"
|
||||
aria-label={t(
|
||||
"storage.cameraStorage.unusedStorageInformation",
|
||||
)}
|
||||
>
|
||||
<CiCircleAlert
|
||||
className="size-5"
|
||||
aria-label="Unused Storage Information"
|
||||
aria-label={t(
|
||||
"storage.cameraStorage.unusedStorageInformation",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@ -71,7 +71,7 @@ export default function IconPicker({
|
||||
{!selectedIcon?.name || !selectedIcon?.Icon ? (
|
||||
<Button
|
||||
className="mt-2 w-full text-muted-foreground"
|
||||
aria-label="Select an icon"
|
||||
aria-label={t("iconPicker.selectIcon")}
|
||||
>
|
||||
{t("iconPicker.selectIcon")}
|
||||
</Button>
|
||||
|
||||
@ -79,14 +79,17 @@ export function SaveSearchDialog({
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button aria-label="Cancel" onClick={onClose}>
|
||||
<Button
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="select"
|
||||
className="mb-2 md:mb-0"
|
||||
aria-label="Save this search"
|
||||
aria-label={t("search.saveSearch.button.save.label")}
|
||||
>
|
||||
{t("button.save", { ns: "common" })}
|
||||
</Button>
|
||||
|
||||
@ -117,7 +117,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Set Password"
|
||||
aria-label={t("menu.user.setPassword")}
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
@ -128,7 +128,7 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
||||
className={
|
||||
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
aria-label={t("menu.user.logout")}
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
|
||||
@ -182,7 +182,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Set Password"
|
||||
aria-label={t("menu.user.setPassword")}
|
||||
onClick={() => setPasswordDialogOpen(true)}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
@ -195,7 +195,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Log out"
|
||||
aria-label={t("menu.user.logout", { ns: "common" })}
|
||||
>
|
||||
<a className="flex" href={logoutUrl}>
|
||||
<LuLogOut className="mr-2 size-4" />
|
||||
@ -216,7 +216,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System metrics"
|
||||
aria-label={t("menu.systemMetrics")}
|
||||
>
|
||||
<LuActivity className="mr-2 size-4" />
|
||||
<span>{t("menu.systemMetrics")}</span>
|
||||
@ -229,7 +229,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="System logs"
|
||||
aria-label={t("menu.systemLogs")}
|
||||
>
|
||||
<LuList className="mr-2 size-4" />
|
||||
<span>{t("menu.systemLogs")}</span>
|
||||
@ -252,7 +252,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Settings"
|
||||
aria-label={t("menu.settings")}
|
||||
>
|
||||
<LuSettings className="mr-2 size-4" />
|
||||
<span>{t("menu.settings")}</span>
|
||||
@ -267,7 +267,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex w-full items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Configuration editor"
|
||||
aria-label={t("menu.configurationEditor")}
|
||||
>
|
||||
<LuSquarePen className="mr-2 size-4" />
|
||||
<span>{t("menu.configurationEditor")}</span>
|
||||
@ -340,7 +340,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Use the system settings for language"
|
||||
aria-label={t("menu.language.withSystem.label")}
|
||||
onClick={() => setLanguage(systemLanguage)}
|
||||
>
|
||||
{language === systemLanguage ? (
|
||||
@ -377,7 +377,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Light mode"
|
||||
aria-label={t("menu.darkMode.light")}
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
@ -397,7 +397,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Dark mode"
|
||||
aria-label={t("menu.darkMode.dark")}
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
@ -417,7 +417,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Use the system settings for light or dark mode"
|
||||
aria-label={t("menu.darkMode.withSystem.label")}
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
{theme === "system" ? (
|
||||
@ -514,7 +514,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
||||
? "cursor-pointer"
|
||||
: "flex items-center p-2 text-sm"
|
||||
}
|
||||
aria-label="Restart Frigate"
|
||||
aria-label={t("menu.restart")}
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<LuRotateCw className="mr-2 size-4" />
|
||||
|
||||
@ -150,7 +150,7 @@ export default function SearchResultActions({
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
aria-label="Delete this tracked object"
|
||||
aria-label={t("itemMenu.deleteTrackedObject.label")}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
<LuTrash2 className="mr-2 size-4" />
|
||||
|
||||
@ -5,6 +5,7 @@ import { IoMdArrowRoundBack } from "react-icons/io";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isPWA } from "@/utils/isPWA";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "i18next";
|
||||
|
||||
const MobilePageContext = createContext<{
|
||||
open: boolean;
|
||||
@ -160,7 +161,7 @@ export function MobilePageHeader({
|
||||
>
|
||||
<Button
|
||||
className="absolute left-0 rounded-lg"
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={handleClose}
|
||||
>
|
||||
|
||||
@ -181,7 +181,7 @@ export default function CameraInfoDialog({
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Copy"
|
||||
aria-label={t("button.copy", { ns: "common" })}
|
||||
onClick={() => onCopyFfprobe()}
|
||||
>
|
||||
{t("button.copy", { ns: "common" })}
|
||||
|
||||
@ -256,7 +256,7 @@ export default function CreateUserDialog({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
disabled={isLoading}
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
@ -265,7 +265,7 @@ export default function CreateUserDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
disabled={isLoading || !form.formState.isValid}
|
||||
className="flex flex-1"
|
||||
type="submit"
|
||||
|
||||
@ -44,7 +44,7 @@ export default function DeleteUserDialog({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -52,7 +52,7 @@ export default function DeleteUserDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
aria-label="Delete"
|
||||
aria-label={t("button.delete", { ns: "common" })}
|
||||
className="flex flex-1"
|
||||
onClick={onDelete}
|
||||
>
|
||||
|
||||
@ -151,7 +151,7 @@ export default function ExportDialog({
|
||||
<Trigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Export"
|
||||
aria-label={t("menu.export", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const now = new Date(latestTime * 1000);
|
||||
@ -327,7 +327,7 @@ export function ExportContent({
|
||||
</div>
|
||||
<Button
|
||||
className={isDesktop ? "" : "w-full"}
|
||||
aria-label="Select or export"
|
||||
aria-label={t("export.selectOrExport")}
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -444,7 +444,7 @@ function CustomTimeSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="Start time"
|
||||
aria-label={t("export.time.start")}
|
||||
variant={startOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -510,7 +510,7 @@ function CustomTimeSelector({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="End time"
|
||||
aria-label={t("export.time.end")}
|
||||
variant={endOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
|
||||
@ -11,6 +11,7 @@ import { GpuInfo, Nvinfo, Vainfo } from "@/types/stats";
|
||||
import { Button } from "../ui/button";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type GPUInfoDialogProps = {
|
||||
showGpuInfo: boolean;
|
||||
@ -22,6 +23,8 @@ export default function GPUInfoDialog({
|
||||
gpuType,
|
||||
setShowGpuInfo,
|
||||
}: GPUInfoDialogProps) {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
|
||||
const { data: vainfo } = useSWR<Vainfo>(
|
||||
showGpuInfo && gpuType == "vainfo" ? "vainfo" : null,
|
||||
);
|
||||
@ -43,13 +46,23 @@ export default function GPUInfoDialog({
|
||||
<Dialog open={showGpuInfo} onOpenChange={setShowGpuInfo}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Vainfo Output</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t("general.hardwareInfo.gpuInfo.vainfoOutput.title")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{vainfo ? (
|
||||
<div className="scrollbar-container mb-2 max-h-96 overflow-y-scroll whitespace-pre-line">
|
||||
<div>Return Code: {vainfo.return_code}</div>
|
||||
<div>
|
||||
{t("general.hardwareInfo.gpuInfo.vainfoOutput.returnCode", {
|
||||
code: vainfo.return_code,
|
||||
})}
|
||||
</div>
|
||||
<br />
|
||||
<div>Process {vainfo.return_code == 0 ? "Output" : "Error"}:</div>
|
||||
<div>
|
||||
{vainfo.return_code == 0
|
||||
? t("general.hardwareInfo.gpuInfo.vainfoOutput.processOutput")
|
||||
: t("general.hardwareInfo.gpuInfo.vainfoOutput.processError")}
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
{vainfo.return_code == 0 ? vainfo.stdout : vainfo.stderr}
|
||||
@ -60,17 +73,17 @@ export default function GPUInfoDialog({
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
aria-label="Close GPU info"
|
||||
aria-label={t("general.hardwareInfo.gpuInfo.closeInfo.label")}
|
||||
onClick={() => setShowGpuInfo(false)}
|
||||
>
|
||||
Close
|
||||
{t("button.close", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Copy GPU info"
|
||||
aria-label={t("general.hardwareInfo.gpuInfo.copyInfo.label")}
|
||||
variant="select"
|
||||
onClick={() => onCopyInfo()}
|
||||
>
|
||||
Copy
|
||||
{t("button.copy", { ns: "common" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@ -81,34 +94,52 @@ export default function GPUInfoDialog({
|
||||
<Dialog open={showGpuInfo} onOpenChange={setShowGpuInfo}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nvidia SMI Output</DialogTitle>
|
||||
<DialogTitle>
|
||||
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.title")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{nvinfo ? (
|
||||
<div className="scrollbar-container mb-2 max-h-96 overflow-y-scroll whitespace-pre-line">
|
||||
<div>Name: {nvinfo["0"].name}</div>
|
||||
<div>
|
||||
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
|
||||
name: nvinfo["0"].name,
|
||||
})}
|
||||
</div>
|
||||
<br />
|
||||
<div>Driver: {nvinfo["0"].driver}</div>
|
||||
<div>
|
||||
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
|
||||
name: nvinfo["0"].driver,
|
||||
})}
|
||||
</div>
|
||||
<br />
|
||||
<div>Cuda Compute Capability: {nvinfo["0"].cuda_compute}</div>
|
||||
<div>
|
||||
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
|
||||
name: nvinfo["0"].cuda_compute,
|
||||
})}
|
||||
</div>
|
||||
<br />
|
||||
<div>VBios Info: {nvinfo["0"].vbios}</div>
|
||||
<div>
|
||||
{t("general.hardwareInfo.gpuInfo.nvidiaSMIOutput.name", {
|
||||
name: nvinfo["0"].vbios,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
aria-label="Close GPU info"
|
||||
aria-label={t("general.hardwareInfo.gpuInfo.closeInfo.label")}
|
||||
onClick={() => setShowGpuInfo(false)}
|
||||
>
|
||||
Close
|
||||
{t("button.close", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Copy GPU info"
|
||||
aria-label={t("general.hardwareInfo.gpuInfo.copyInfo.label")}
|
||||
variant="select"
|
||||
onClick={() => onCopyInfo()}
|
||||
>
|
||||
Copy
|
||||
{t("button.copy", { ns: "common" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@ -3,6 +3,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { t } from "i18next";
|
||||
|
||||
type MobileCameraDrawerProps = {
|
||||
allCameras: string[];
|
||||
@ -25,7 +26,7 @@ export default function MobileCameraDrawer({
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
aria-label="Cameras"
|
||||
aria-label={t("menu.live.cameras")}
|
||||
size="sm"
|
||||
>
|
||||
<FaVideo className="text-secondary-foreground" />
|
||||
|
||||
@ -149,7 +149,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("export") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Export"
|
||||
aria-label={t("export")}
|
||||
onClick={() => {
|
||||
setDrawerMode("export");
|
||||
setMode("select");
|
||||
@ -162,7 +162,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("calendar") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Calendar"
|
||||
aria-label={t("calendar")}
|
||||
variant={filter?.after ? "select" : "default"}
|
||||
onClick={() => setDrawerMode("calendar")}
|
||||
>
|
||||
@ -175,7 +175,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{features.includes("filter") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label="Filter"
|
||||
aria-label={t("filter")}
|
||||
variant={filter?.labels || filter?.zones ? "select" : "default"}
|
||||
onClick={() => setDrawerMode("filter")}
|
||||
>
|
||||
@ -247,7 +247,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<SelectSeparator />
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Button
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={() => {
|
||||
onUpdateFilter({
|
||||
...filter,
|
||||
@ -272,7 +272,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
<div className="absolute left-1/2 -translate-x-1/2 text-muted-foreground">
|
||||
Filter
|
||||
{t("filter")}
|
||||
</div>
|
||||
</div>
|
||||
<GeneralFilterContent
|
||||
@ -326,7 +326,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
className="rounded-lg capitalize"
|
||||
aria-label="Filters"
|
||||
aria-label={t("filters")}
|
||||
variant={
|
||||
filter?.labels || filter?.after || filter?.zones
|
||||
? "select"
|
||||
|
||||
@ -86,7 +86,7 @@ export default function RoleChangeDialog({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -94,7 +94,7 @@ export default function RoleChangeDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
className="flex flex-1"
|
||||
onClick={() => onSave(selectedRole)}
|
||||
disabled={selectedRole === currentRole}
|
||||
|
||||
@ -30,7 +30,7 @@ export default function SaveExportOverlay({
|
||||
>
|
||||
<Button
|
||||
className="flex items-center gap-1 text-primary"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={onCancel}
|
||||
>
|
||||
@ -39,7 +39,7 @@ export default function SaveExportOverlay({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Preview export"
|
||||
aria-label={t("export.fromTimeline.previewExport")}
|
||||
size="sm"
|
||||
onClick={onPreview}
|
||||
>
|
||||
@ -48,13 +48,13 @@ export default function SaveExportOverlay({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-1"
|
||||
aria-label="Save export"
|
||||
aria-label={t("export.fromTimeline.saveExport")}
|
||||
variant="select"
|
||||
size="sm"
|
||||
onClick={onSave}
|
||||
>
|
||||
<FaCompactDisc />
|
||||
{t("export.fromTimeline.saveExport", { ns: "components/dialog" })}
|
||||
{t("export.fromTimeline.saveExport")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -201,7 +201,7 @@ export default function SetPasswordDialog({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -209,7 +209,7 @@ export default function SetPasswordDialog({
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
className="flex flex-1"
|
||||
onClick={handleSave}
|
||||
disabled={!password || password !== confirmPassword}
|
||||
|
||||
@ -26,6 +26,7 @@ import { Button } from "@/components/ui/button";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type AnnotationSettingsPaneProps = {
|
||||
event: Event;
|
||||
@ -41,6 +42,8 @@ export function AnnotationSettingsPane({
|
||||
annotationOffset,
|
||||
setAnnotationOffset,
|
||||
}: AnnotationSettingsPaneProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
const { data: config, mutate: updateConfig } =
|
||||
useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -81,9 +84,15 @@ export function AnnotationSettingsPane({
|
||||
);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -91,7 +100,7 @@ export function AnnotationSettingsPane({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to save config changes: ${errorMessage}`, {
|
||||
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
@ -99,7 +108,7 @@ export function AnnotationSettingsPane({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig, config, event],
|
||||
[updateConfig, config, event, t],
|
||||
);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@ -126,7 +135,7 @@ export function AnnotationSettingsPane({
|
||||
return (
|
||||
<div className="mb-3 space-y-3 rounded-lg border border-secondary-foreground bg-background_alt p-2">
|
||||
<Heading as="h4" className="my-2">
|
||||
Annotation Settings
|
||||
{t("objectLifecycle.annotationSettings.title")}
|
||||
</Heading>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center justify-start gap-2 p-3">
|
||||
@ -136,11 +145,11 @@ export function AnnotationSettingsPane({
|
||||
onCheckedChange={setShowZones}
|
||||
/>
|
||||
<Label className="cursor-pointer" htmlFor="show-zones">
|
||||
Show All Zones
|
||||
{t("objectLifecycle.annotationSettings.showAllZones")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Always show zones on frames where objects have entered a zone.
|
||||
{t("objectLifecycle.annotationSettings.showAllZones.desc")}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
@ -154,17 +163,16 @@ export function AnnotationSettingsPane({
|
||||
name="annotationOffset"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Annotation Offset</FormLabel>
|
||||
<FormLabel>
|
||||
{t("objectLifecycle.annotationSettings.offset.label")}
|
||||
</FormLabel>
|
||||
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
|
||||
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-0 md:my-5">
|
||||
<PiWarningCircle className="size-24" />
|
||||
<div>
|
||||
This data comes from your camera's detect feed but is
|
||||
overlayed on images from the the record feed. It is
|
||||
unlikely that the two streams are perfectly in sync. As a
|
||||
result, the bounding box and the footage will not line up
|
||||
perfectly. However, the <code>annotation_offset</code>{" "}
|
||||
field can be used to adjust this.
|
||||
<Trans ns="views/explore">
|
||||
objectLifecycle.annotationSettings.offset.desc
|
||||
</Trans>
|
||||
<div className="mt-2 flex items-center text-primary">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/reference"
|
||||
@ -172,7 +180,9 @@ export function AnnotationSettingsPane({
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
Read the documentation{" "}
|
||||
{t(
|
||||
"objectLifecycle.annotationSettings.offset.documentation",
|
||||
)}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
@ -187,16 +197,11 @@ export function AnnotationSettingsPane({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Milliseconds to offset detect annotations by.{" "}
|
||||
<em>Default: 0</em>
|
||||
{t(
|
||||
"objectLifecycle.annotationSettings.offset.millisecondsToOffset",
|
||||
)}
|
||||
<div className="mt-2">
|
||||
TIP: Imagine there is an event clip with a person
|
||||
walking from left to right. If the event timeline
|
||||
bounding box is consistently to the left of the person
|
||||
then the value should be decreased. Similarly, if a
|
||||
person is walking from left to right and the bounding
|
||||
box is consistently ahead of the person then the value
|
||||
should be increased.
|
||||
{t("objectLifecycle.annotationSettings.offset.tips")}
|
||||
</div>
|
||||
</FormDescription>
|
||||
</div>
|
||||
@ -210,14 +215,14 @@ export function AnnotationSettingsPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
onClick={form.handleSubmit(onApply)}
|
||||
>
|
||||
Apply
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
type="submit"
|
||||
@ -225,10 +230,10 @@ export function AnnotationSettingsPane({
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ActivityIndicator />
|
||||
<span>Saving...</span>
|
||||
<span>{t("button.saving", { ns: "common" })}</span>
|
||||
</div>
|
||||
) : (
|
||||
"Save"
|
||||
t("button.save", { ns: "common" })
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -54,6 +54,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { ObjectPath } from "./ObjectPath";
|
||||
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
||||
import { IoPlayCircleOutline } from "react-icons/io5";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type ObjectLifecycleProps = {
|
||||
className?: string;
|
||||
@ -68,6 +69,8 @@ export default function ObjectLifecycle({
|
||||
fullscreen = false,
|
||||
setPane,
|
||||
}: ObjectLifecycleProps) {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
|
||||
const { data: eventSequence } = useSWR<ObjectLifecycleSequence[]>([
|
||||
"timeline",
|
||||
{
|
||||
@ -334,12 +337,16 @@ export default function ObjectLifecycle({
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
<Button
|
||||
className="mb-2 mt-3 flex items-center gap-2.5 rounded-lg md:mt-0"
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => setPane("overview")}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{t("button.back", { ns: "common" })}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -361,7 +368,7 @@ export default function ObjectLifecycle({
|
||||
<div className="relative aspect-video">
|
||||
<div className="flex flex-col items-center justify-center p-20 text-center">
|
||||
<LuFolderX className="size-16" />
|
||||
No image found for this timestamp.
|
||||
{t("objectLifecycle.noImageFound")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -468,7 +475,9 @@ export default function ObjectLifecycle({
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="text-primary">Create Object Mask</div>
|
||||
<div className="text-primary">
|
||||
{t("objectLifecycle.createObjectMask")}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
@ -477,7 +486,7 @@ export default function ObjectLifecycle({
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-row items-center justify-between">
|
||||
<Heading as="h4">Object Lifecycle</Heading>
|
||||
<Heading as="h4">{t("objectLifecycle.title")}</Heading>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<Tooltip>
|
||||
@ -485,7 +494,7 @@ export default function ObjectLifecycle({
|
||||
<Button
|
||||
variant={showControls ? "select" : "default"}
|
||||
className="size-7 p-1.5"
|
||||
aria-label="Adjust annotation settings"
|
||||
aria-label={t("objectLifecycle.adjustAnnotationSettings")}
|
||||
>
|
||||
<LuSettings
|
||||
className="size-5"
|
||||
@ -494,14 +503,16 @@ export default function ObjectLifecycle({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>Adjust annotation settings</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t("objectLifecycle.adjustAnnotationSettings")}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="mb-2 text-sm text-muted-foreground">
|
||||
Scroll to view the significant moments of this object's lifecycle.
|
||||
{t("objectLifecycle.scrollViewTips")}
|
||||
</div>
|
||||
<div className="min-w-20 text-right text-sm text-muted-foreground">
|
||||
{current + 1} of {eventSequence.length}
|
||||
@ -509,7 +520,7 @@ export default function ObjectLifecycle({
|
||||
</div>
|
||||
{config?.cameras[event.camera]?.onvif.autotracking.enabled_in_config && (
|
||||
<div className="-mt-2 mb-2 text-sm text-danger">
|
||||
Bounding box positions will be inaccurate for autotracking cameras.
|
||||
{t("objectLifecycle.autoTrackingTips")}
|
||||
</div>
|
||||
)}
|
||||
{showControls && (
|
||||
@ -559,8 +570,8 @@ export default function ObjectLifecycle({
|
||||
timezone: config.ui.timezone,
|
||||
strftime_fmt:
|
||||
config.ui.time_format == "24hour"
|
||||
? "%d %b %H:%M:%S"
|
||||
: "%m/%d %I:%M:%S%P",
|
||||
? t("time.formattedTimestamp2.24hour")
|
||||
: t("time.formattedTimestamp2"),
|
||||
time_style: "medium",
|
||||
date_style: "medium",
|
||||
})}
|
||||
|
||||
@ -193,7 +193,7 @@ export default function ReviewDetailDialog({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label="Share this review item"
|
||||
aria-label={t("details.item.button.share")}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
shareOrCopy(`${baseUrl}review?id=${review.id}`)
|
||||
|
||||
@ -677,7 +677,7 @@ function ObjectDetailsTab({
|
||||
<div className="flex items-start">
|
||||
<Button
|
||||
className="rounded-r-none border-r-0"
|
||||
aria-label="Regenerate tracked object description"
|
||||
aria-label={t("details.button.regenerate.label")}
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
{t("details.button.regenerate")}
|
||||
@ -687,7 +687,7 @@ function ObjectDetailsTab({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="rounded-l-none border-l-0 px-2"
|
||||
aria-label="Expand regeneration menu"
|
||||
aria-label={t("details.expandRegenerationMenu")}
|
||||
>
|
||||
<FaChevronDown className="size-3" />
|
||||
</Button>
|
||||
@ -695,14 +695,14 @@ function ObjectDetailsTab({
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
aria-label="Regenerate from snapshot"
|
||||
aria-label={t("details.regenerateFromSnapshot")}
|
||||
onClick={() => regenerateDescription("snapshot")}
|
||||
>
|
||||
{t("details.regenerateFromSnapshot")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
aria-label="Regenerate from thumbnails"
|
||||
aria-label={t("details.regenerateFromThumbnails")}
|
||||
onClick={() => regenerateDescription("thumbnails")}
|
||||
>
|
||||
{t("details.regenerateFromThumbnails")}
|
||||
@ -716,7 +716,7 @@ function ObjectDetailsTab({
|
||||
!config?.cameras[search.camera].genai.enabled) && (
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
onClick={updateDescription}
|
||||
>
|
||||
{t("button.save", { ns: "common" })}
|
||||
@ -867,7 +867,7 @@ export function ObjectSnapshotTab({
|
||||
<>
|
||||
<Button
|
||||
className="bg-success"
|
||||
aria-label="Confirm this label for Frigate Plus"
|
||||
aria-label={t("explore.plus.review.true.label")}
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
@ -883,7 +883,7 @@ export function ObjectSnapshotTab({
|
||||
</Button>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Do not confirm this label for Frigate Plus"
|
||||
aria-label={t("explore.plus.review.false.label")}
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
|
||||
@ -116,7 +116,7 @@ export default function RestartDialog({
|
||||
<Button
|
||||
size="lg"
|
||||
className="mt-5"
|
||||
aria-label="Force reload now"
|
||||
aria-label={t("restart.restarting.button")}
|
||||
onClick={handleForceReload}
|
||||
>
|
||||
{t("restart.restarting.button")}
|
||||
|
||||
@ -85,7 +85,7 @@ export default function SearchFilterDialog({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="More Filters"
|
||||
aria-label={t("more")}
|
||||
size="sm"
|
||||
variant={moreFiltersSelected ? "select" : "default"}
|
||||
>
|
||||
@ -167,7 +167,7 @@ export default function SearchFilterDialog({
|
||||
<div className="flex items-center justify-evenly p-2">
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
onClick={() => {
|
||||
if (currentFilter != filter) {
|
||||
onUpdateFilter(currentFilter);
|
||||
@ -179,7 +179,7 @@ export default function SearchFilterDialog({
|
||||
{t("button.apply", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Reset filters to default values"
|
||||
aria-label={t("reset.label")}
|
||||
onClick={() => {
|
||||
setCurrentFilter((prevFilter) => ({
|
||||
...prevFilter,
|
||||
@ -287,7 +287,9 @@ function TimeRangeFilterContent({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"} `}
|
||||
aria-label="Select Start Time"
|
||||
aria-label={t("export.time.start.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
variant={startOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -325,7 +327,9 @@ function TimeRangeFilterContent({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={`text-primary ${isDesktop ? "" : "text-xs"}`}
|
||||
aria-label="Select End Time"
|
||||
aria-label={t("export.time.end.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
variant={endOpen ? "select" : "default"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@ -688,14 +692,14 @@ export function SnapshotClipFilterContent({
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="yes"
|
||||
aria-label="Yes"
|
||||
aria-label={t("button.yes", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.yes", { ns: "common" })}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
aria-label={t("button.no", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.no", { ns: "common" })}
|
||||
@ -766,14 +770,14 @@ export function SnapshotClipFilterContent({
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="yes"
|
||||
aria-label="Yes"
|
||||
aria-label={t("button.yes", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.yes", { ns: "common" })}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
aria-label={t("button.no", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.no", { ns: "common" })}
|
||||
@ -821,14 +825,14 @@ export function SnapshotClipFilterContent({
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="yes"
|
||||
aria-label="Yes"
|
||||
aria-label={t("button.yes", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.yes", { ns: "common" })}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="no"
|
||||
aria-label="No"
|
||||
aria-label={t("button.no", { ns: "common" })}
|
||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||
>
|
||||
{t("button.no", { ns: "common" })}
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
} from "../ui/alert-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FaCompress, FaExpand } from "react-icons/fa";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type VideoControls = {
|
||||
volume?: boolean;
|
||||
@ -309,6 +310,8 @@ function FrigatePlusUploadButton({
|
||||
onUploadFrame,
|
||||
containerRef,
|
||||
}: FrigatePlusUploadButtonProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
|
||||
const [videoImg, setVideoImg] = useState<string>();
|
||||
|
||||
return (
|
||||
@ -346,14 +349,16 @@ function FrigatePlusUploadButton({
|
||||
className="md:max-w-2xl lg:max-w-3xl xl:max-w-4xl"
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Submit this frame to Frigate+?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("submitFrigatePlus.title")}</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<img className="aspect-video w-full object-contain" src={videoImg} />
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction className="bg-selected" onClick={onUploadFrame}>
|
||||
Submit
|
||||
{t("submitFrigatePlus.submit")}
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@ -358,14 +358,14 @@ export function CameraStreamingDialog({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
onClick={handleSave}
|
||||
|
||||
@ -176,9 +176,15 @@ export default function MotionMaskEditPane({
|
||||
);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -186,7 +192,7 @@ export default function MotionMaskEditPane({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to save config changes: ${errorMessage}`, {
|
||||
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
@ -323,14 +329,14 @@ export default function MotionMaskEditPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
type="submit"
|
||||
|
||||
@ -364,7 +364,7 @@ export default function ObjectMaskEditPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
@ -373,7 +373,7 @@ export default function ObjectMaskEditPane({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -4,6 +4,7 @@ import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
|
||||
import { Button } from "../ui/button";
|
||||
import { TbPolygon, TbPolygonOff } from "react-icons/tb";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PolygonEditControlsProps = {
|
||||
polygons: Polygon[];
|
||||
@ -20,6 +21,7 @@ export default function PolygonEditControls({
|
||||
snapPoints,
|
||||
setSnapPoints,
|
||||
}: PolygonEditControlsProps) {
|
||||
const { t } = useTranslation(["views/settings"]);
|
||||
const undo = () => {
|
||||
if (activePolygonIndex === undefined || !polygons) {
|
||||
return;
|
||||
@ -80,35 +82,37 @@ export default function PolygonEditControls({
|
||||
<Button
|
||||
variant="default"
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label="Remove last point"
|
||||
aria-label={t("masksAndZones.form.polygonDrawing.removeLastPoint")}
|
||||
disabled={!polygons[activePolygonIndex].points.length}
|
||||
onClick={undo}
|
||||
>
|
||||
<MdUndo className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove last point</TooltipContent>
|
||||
<TooltipContent>
|
||||
{t("masksAndZones.form.polygonDrawing.removeLastPoint")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label="Clear all points"
|
||||
aria-label={t("masksAndZones.form.polygonDrawing.reset.label")}
|
||||
disabled={!polygons[activePolygonIndex].points.length}
|
||||
onClick={reset}
|
||||
>
|
||||
<MdOutlineRestartAlt className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reset</TooltipContent>
|
||||
<TooltipContent>{t("button.reset", { ns: "common" })}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={snapPoints ? "select" : "default"}
|
||||
className={cn("size-6 rounded-md p-1")}
|
||||
aria-label="Snap points"
|
||||
aria-label={t("masksAndZones.form.polygonDrawing.snapPoints.true")}
|
||||
onClick={() => setSnapPoints((prev) => !prev)}
|
||||
>
|
||||
{snapPoints ? (
|
||||
@ -119,7 +123,9 @@ export default function PolygonEditControls({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{snapPoints ? "Don't snap points" : "Snap points"}
|
||||
{snapPoints
|
||||
? t("masksAndZones.form.polygonDrawing.snapPoints.false")
|
||||
: t("masksAndZones.form.polygonDrawing.snapPoints.true")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@ -36,7 +36,7 @@ import { reviewQueries } from "@/utils/zoneEdutUtil";
|
||||
import IconWrapper from "../ui/icon-wrapper";
|
||||
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
|
||||
import { buttonVariants } from "../ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type PolygonItemProps = {
|
||||
polygon: Polygon;
|
||||
@ -177,14 +177,25 @@ export default function PolygonItem({
|
||||
.put(`config/set?${url}`, { requires_restart: 0 })
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
toast.success(`${polygon?.name} has been deleted.`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.success(
|
||||
t("masksAndZones.form.polygonDrawing.delete.success", {
|
||||
name: polygon?.name,
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -192,7 +203,7 @@ export default function PolygonItem({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to save config changes: ${errorMessage}`, {
|
||||
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
@ -200,7 +211,7 @@ export default function PolygonItem({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig, cameraConfig],
|
||||
[updateConfig, cameraConfig, t],
|
||||
);
|
||||
|
||||
const handleDelete = () => {
|
||||
@ -255,19 +266,30 @@ export default function PolygonItem({
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
||||
<AlertDialogTitle>
|
||||
{t("masksAndZones.form.polygonDrawing.delete.title")}
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the{" "}
|
||||
{polygon.type.replace("_", " ")} <em>{polygon.name}</em>?
|
||||
<Trans
|
||||
ns="views/settings"
|
||||
values={{
|
||||
type: polygon.type.replace("_", " "),
|
||||
name: polygon.name,
|
||||
}}
|
||||
>
|
||||
masksAndZones.form.polygonDrawing.delete.desc
|
||||
</Trans>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "destructive" })}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@ -281,26 +303,26 @@ export default function PolygonItem({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
aria-label="Edit"
|
||||
aria-label={t("button.edit", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setActivePolygonIndex(index);
|
||||
setEditPane(polygon.type);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
{t("button.edit", { ns: "common" })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
aria-label="Copy"
|
||||
aria-label={t("button.copy", { ns: "common" })}
|
||||
onClick={() => handleCopyCoordinates(index)}
|
||||
>
|
||||
Copy
|
||||
{t("button.copy", { ns: "common" })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
aria-label="Delete"
|
||||
aria-label={t("button.delete", { ns: "common" })}
|
||||
disabled={isLoading}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
{t("button.delete", { ns: "common" })}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -48,7 +48,7 @@ export default function ExploreSettings({
|
||||
const trigger = (
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Explore Settings"
|
||||
aria-label={t("explore.settings.title")}
|
||||
size="sm"
|
||||
>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
|
||||
@ -813,7 +813,7 @@ export default function ZoneEditPane({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.cancel", { ns: "common" })}
|
||||
@ -822,7 +822,7 @@ export default function ZoneEditPane({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -420,7 +420,7 @@ export function DateRangePicker({
|
||||
<div className="mx-auto flex w-64 items-center justify-evenly gap-2 py-2">
|
||||
<Button
|
||||
variant="select"
|
||||
aria-label="Apply"
|
||||
aria-label={t("button.apply", { ns: "common" })}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
if (
|
||||
@ -440,7 +440,7 @@ export function DateRangePicker({
|
||||
onReset?.();
|
||||
}}
|
||||
variant="ghost"
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
>
|
||||
{t("button.reset", { ns: "common"})}
|
||||
</Button>
|
||||
|
||||
@ -6,6 +6,7 @@ import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
@ -196,6 +197,7 @@ const CarouselPrevious = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
@ -210,13 +212,13 @@ const CarouselPrevious = React.forwardRef<
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
aria-label="Previous slide"
|
||||
aria-label={t("objectLifecycle.carousel.previous")}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
<span className="sr-only">{t("objectLifecycle.carousel.previous")}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
@ -226,6 +228,7 @@ const CarouselNext = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
|
||||
const { t } = useTranslation(["views/explore"]);
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
@ -240,13 +243,13 @@ const CarouselNext = React.forwardRef<
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
aria-label="Next slide"
|
||||
aria-label={t("objectLifecycle.carousel.next")}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
<span className="sr-only">Next slide</span>
|
||||
<span className="sr-only">{t("objectLifecycle.carousel.next")}</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
@ -3,11 +3,12 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
import { t } from "i18next"
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
aria-label={t("pagination.label")}
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
@ -64,13 +65,13 @@ const PaginationPrevious = ({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
aria-label={t("pagination.previous.label")}
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
<span>{t("pagination.previous")}</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
@ -80,12 +81,12 @@ const PaginationNext = ({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
aria-label={t("pagination.next.label")}
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<span>{t("pagination.next")}</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
@ -101,7 +102,7 @@ const PaginationEllipsis = ({
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
<span className="sr-only">{t("pagination.more")}</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
@ -198,7 +198,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Copy config"
|
||||
aria-label={t("copyConfig")}
|
||||
onClick={() => handleCopyConfig()}
|
||||
>
|
||||
<LuCopy className="text-secondary-foreground" />
|
||||
@ -207,7 +207,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Save and restart"
|
||||
aria-label={t("saveAndRestart")}
|
||||
onClick={() => setRestartDialogOpen(true)}
|
||||
>
|
||||
<div className="relative size-5">
|
||||
@ -219,7 +219,7 @@ function ConfigEditor() {
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Save only without restarting"
|
||||
aria-label={t("saveOnly")}
|
||||
onClick={() => onHandleSaveConfig("saveonly")}
|
||||
>
|
||||
<LuSave className="text-secondary-foreground" />
|
||||
|
||||
@ -34,8 +34,10 @@ import { debounce } from "lodash";
|
||||
import { isIOS, isMobile } from "react-device-detect";
|
||||
import { isPWA } from "@/utils/isPWA";
|
||||
import { isInIframe } from "@/utils/isIFrame";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function Logs() {
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
const [logService, setLogService] = useState<LogType>("frigate");
|
||||
const tabsRef = useRef<HTMLDivElement | null>(null);
|
||||
const lazyLogWrapperRef = useRef<HTMLDivElement>(null);
|
||||
@ -285,13 +287,13 @@ function Logs() {
|
||||
fetchInitialLogs()
|
||||
.then(() => {
|
||||
copy(logs.join("\n"));
|
||||
toast.success("Copied logs to clipboard");
|
||||
toast.success(t("logs.copy.success"));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Could not copy logs to clipboard");
|
||||
toast.error(t("logs.copy.error"));
|
||||
});
|
||||
}
|
||||
}, [logs, fetchInitialLogs]);
|
||||
}, [logs, fetchInitialLogs, t]);
|
||||
|
||||
const handleDownloadLogs = useCallback(() => {
|
||||
axios
|
||||
@ -496,23 +498,25 @@ function Logs() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center justify-between gap-2"
|
||||
aria-label="Copy logs to clipboard"
|
||||
aria-label={t("logs.copy.label")}
|
||||
size="sm"
|
||||
onClick={handleCopyLogs}
|
||||
>
|
||||
<FaCopy className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">
|
||||
Copy to Clipboard
|
||||
{t("logs.copy.label")}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center justify-between gap-2"
|
||||
aria-label="Download logs"
|
||||
aria-label={t("logs.download.label")}
|
||||
size="sm"
|
||||
onClick={handleDownloadLogs}
|
||||
>
|
||||
<FaDownload className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">Download</div>
|
||||
<div className="hidden text-primary md:block">
|
||||
{t("button.download", { ns: "common" })}
|
||||
</div>
|
||||
</Button>
|
||||
<LogSettingsButton
|
||||
selectedLabels={filterSeverity}
|
||||
@ -527,8 +531,10 @@ function Logs() {
|
||||
<div className="grid grid-cols-5 *:px-0 *:py-3 *:text-sm *:text-primary/40 md:grid-cols-12">
|
||||
<div className="col-span-3 lg:col-span-2">
|
||||
<div className="flex w-full flex-row items-center">
|
||||
<div className="ml-1 min-w-16 capitalize lg:min-w-20">Type</div>
|
||||
<div className="mr-3">Timestamp</div>
|
||||
<div className="ml-1 min-w-16 capitalize lg:min-w-20">
|
||||
{t("logs.type.label")}
|
||||
</div>
|
||||
<div className="mr-3">{t("logs.type.timestamp")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@ -537,7 +543,7 @@ function Logs() {
|
||||
logService == "frigate" ? "col-span-2" : "col-span-1",
|
||||
)}
|
||||
>
|
||||
Tag
|
||||
{t("logs.type.tag")}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@ -547,7 +553,7 @@ function Logs() {
|
||||
: "md:col-span-8 lg:col-span-9",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-1">Message</div>
|
||||
<div className="flex flex-1">{t("logs.type.message")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -566,9 +572,7 @@ function Logs() {
|
||||
<TooltipTrigger>
|
||||
<MdCircle className="mr-2 size-2 animate-pulse cursor-default text-selected shadow-selected drop-shadow-md" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Logs are streaming from the server
|
||||
</TooltipContent>
|
||||
<TooltipContent>{t("logs.tips")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { ObjectLifecycleSequence } from "@/types/timeline";
|
||||
import { t } from "i18next";
|
||||
|
||||
export function getLifecycleItemDescription(
|
||||
lifecycleItem: ObjectLifecycleSequence,
|
||||
) {
|
||||
// can't use useTranslation here
|
||||
const label = (
|
||||
(Array.isArray(lifecycleItem.data.sub_label)
|
||||
? lifecycleItem.data.sub_label[0]
|
||||
@ -11,37 +13,63 @@ export function getLifecycleItemDescription(
|
||||
|
||||
switch (lifecycleItem.class_type) {
|
||||
case "visible":
|
||||
return `${label} detected`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.visible", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
case "entered_zone":
|
||||
return `${label} entered ${lifecycleItem.data.zones
|
||||
.join(" and ")
|
||||
.replaceAll("_", " ")}`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.entered_zone", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
zones: lifecycleItem.data.zones.join(" and ").replaceAll("_", " "),
|
||||
});
|
||||
case "active":
|
||||
return `${label} became active`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.active", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
case "stationary":
|
||||
return `${label} became stationary`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.stationary", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
case "attribute": {
|
||||
let title = "";
|
||||
if (
|
||||
lifecycleItem.data.attribute == "face" ||
|
||||
lifecycleItem.data.attribute == "license_plate"
|
||||
) {
|
||||
title = `${lifecycleItem.data.attribute.replaceAll(
|
||||
"_",
|
||||
" ",
|
||||
)} detected for ${label}`;
|
||||
title = t(
|
||||
"objectLifecycle.lifecycleItemDesc.attribute.faceOrLicense_plate",
|
||||
{
|
||||
label,
|
||||
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
|
||||
ns: "views/explore",
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = `${
|
||||
lifecycleItem.data.label
|
||||
} recognized as ${lifecycleItem.data.attribute.replaceAll("_", " ")}`;
|
||||
title = t("objectLifecycle.lifecycleItemDesc.attribute.other", {
|
||||
label: lifecycleItem.data.label,
|
||||
attribute: lifecycleItem.data.attribute.replaceAll("_", " "),
|
||||
ns: "views/explore",
|
||||
});
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "gone":
|
||||
return `${label} left`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.gone", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
case "heard":
|
||||
return `${label} heard`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.heard", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
case "external":
|
||||
return `${label} detected`;
|
||||
return t("objectLifecycle.lifecycleItemDesc.external", {
|
||||
label,
|
||||
ns: "views/explore",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,7 +277,7 @@ export default function EventView({
|
||||
<ToggleGroupItem
|
||||
className={cn(severityToggle != "alert" && "text-muted-foreground")}
|
||||
value="alert"
|
||||
aria-label="Select alerts"
|
||||
aria-label={t("alerts")}
|
||||
>
|
||||
{isMobileOnly ? (
|
||||
<div
|
||||
@ -311,7 +311,7 @@ export default function EventView({
|
||||
severityToggle != "detection" && "text-muted-foreground",
|
||||
)}
|
||||
value="detection"
|
||||
aria-label="Select detections"
|
||||
aria-label={t("detections")}
|
||||
>
|
||||
{isMobileOnly ? (
|
||||
<div
|
||||
@ -348,7 +348,7 @@ export default function EventView({
|
||||
severityToggle != "significant_motion" && "text-muted-foreground",
|
||||
)}
|
||||
value="significant_motion"
|
||||
aria-label="Select motion"
|
||||
aria-label={t("motion.label")}
|
||||
>
|
||||
{isMobileOnly ? (
|
||||
<GiSoundWaves className="size-6 rotate-90 text-severity_significant_motion" />
|
||||
@ -792,14 +792,14 @@ function DetectionReview({
|
||||
<div className="col-span-full flex items-center justify-center">
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Mark these items as reviewed"
|
||||
aria-label={t("markTheseItemsAsReviewed")}
|
||||
variant="select"
|
||||
onClick={() => {
|
||||
setSelectedReviews([]);
|
||||
markAllItemsAsReviewed(currentItems ?? []);
|
||||
}}
|
||||
>
|
||||
Mark these items as reviewed
|
||||
{t("markTheseItemsAsReviewed")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
isDesktop,
|
||||
@ -144,12 +145,14 @@ export default function LiveBirdseyeView({
|
||||
{!fullscreen ? (
|
||||
<Button
|
||||
className={`flex items-center gap-2 rounded-lg ${isMobile ? "ml-2" : "ml-0"}`}
|
||||
aria-label="Go Back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size={isMobile ? "icon" : "sm"}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowBack className="size-5" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
{isDesktop && (
|
||||
<div className="text-primary">{t("button.back")}</div>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@ -118,7 +118,7 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type LiveCameraViewProps = {
|
||||
config?: FrigateConfig;
|
||||
@ -430,7 +430,7 @@ export default function LiveCameraView({
|
||||
>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -443,7 +443,7 @@ export default function LiveCameraView({
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Show historical footage"
|
||||
aria-label={t("history.label")}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate("review", {
|
||||
@ -476,7 +476,7 @@ export default function LiveCameraView({
|
||||
{fullscreen && (
|
||||
<Button
|
||||
className="bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-primary"
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -876,14 +876,19 @@ function PtzControlPanel({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
||||
aria-label="Click in the frame to center the camera"
|
||||
aria-label={t("ptz.move.clickMove.label")}
|
||||
onClick={() => setClickOverlay(!clickOverlay)}
|
||||
>
|
||||
<TbViewfinder />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{clickOverlay ? "Disable" : "Enable"} click to move</p>
|
||||
<p>
|
||||
{clickOverlay
|
||||
? t("ptz.move.clickMove.disable")
|
||||
: t("ptz.move.clickMove.enable")}{" "}
|
||||
click to move
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -894,7 +899,7 @@ function PtzControlPanel({
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenu modal={!isDesktop}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-label="PTZ camera presets">
|
||||
<Button aria-label={t("ptz.presets")}>
|
||||
<BsThreeDotsVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@ -916,7 +921,7 @@ function PtzControlPanel({
|
||||
</DropdownMenu>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>PTZ camera presets</p>
|
||||
<p>{t("ptz.presets")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@ -926,6 +931,7 @@ function PtzControlPanel({
|
||||
}
|
||||
|
||||
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
||||
const { t } = useTranslation(["views/live"]);
|
||||
const rankMap = { all: 0, motion: 1, active_objects: 2 };
|
||||
const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
|
||||
const mode = retain?.mode;
|
||||
@ -940,13 +946,25 @@ function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
||||
? recordRetainMode
|
||||
: alertsRetainMode;
|
||||
|
||||
const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
|
||||
const source =
|
||||
effectiveRetainMode === recordRetainMode
|
||||
? t("camera", { ns: "views/events" })
|
||||
: t("alerts", { ns: "views/events" });
|
||||
|
||||
return effectiveRetainMode !== "all" ? (
|
||||
<div>
|
||||
Your {source} recording retention configuration is set to{" "}
|
||||
<code>mode: {effectiveRetainMode}</code>, so this on-demand recording will
|
||||
only keep segments with {effectiveRetainMode.replaceAll("_", " ")}.
|
||||
<Trans
|
||||
ns="views/live"
|
||||
values={{
|
||||
source,
|
||||
effectiveRetainMode,
|
||||
effectiveRetainModeName: t(
|
||||
"effectiveRetainMode.modes." + effectiveRetainMode,
|
||||
),
|
||||
}}
|
||||
>
|
||||
effectiveRetainMode.notAllTips
|
||||
</Trans>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
@ -1034,9 +1052,7 @@ function FrigateCameraFeatures({
|
||||
setIsRecording(true);
|
||||
const toastId = toast.success(
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="font-semibold">
|
||||
Started manual on-demand recording.
|
||||
</div>
|
||||
<div className="font-semibold">{t("manualRecording.started")}</div>
|
||||
{!camera.record.enabled || camera.record.alerts.retain.days == 0 ? (
|
||||
<div>{t("manualRecording.recordDisabledTips")}</div>
|
||||
) : (
|
||||
|
||||
@ -395,7 +395,7 @@ export function RecordingView({
|
||||
<div className={cn("flex items-center gap-2")}>
|
||||
<Button
|
||||
className="flex items-center gap-2.5 rounded-lg"
|
||||
aria-label="Go back"
|
||||
aria-label={t("label.back", { ns: "common" })}
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
@ -492,14 +492,14 @@ export function RecordingView({
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "timeline" ? "" : "text-muted-foreground"}`}
|
||||
value="timeline"
|
||||
aria-label="Select timeline"
|
||||
aria-label={t("timeline.aria")}
|
||||
>
|
||||
<div className="">{t("timeline")}</div>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
|
||||
value="events"
|
||||
aria-label="Select events"
|
||||
aria-label={t("events.aria")}
|
||||
>
|
||||
<div className="">{t("events.label")}</div>
|
||||
</ToggleGroupItem>
|
||||
|
||||
@ -202,7 +202,7 @@ export default function AuthenticationView() {
|
||||
</div>
|
||||
<Button
|
||||
className="flex items-center gap-2 self-start sm:self-auto"
|
||||
aria-label="Add a new user"
|
||||
aria-label={t("users.addUser")}
|
||||
variant="default"
|
||||
onClick={() => setShowCreate(true)}
|
||||
>
|
||||
|
||||
@ -633,7 +633,7 @@ export default function CameraSettingsView({
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -643,7 +643,7 @@ export default function CameraSettingsView({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -518,7 +518,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new zone"
|
||||
aria-label={t("masksAndZones.zones.add")}
|
||||
onClick={() => {
|
||||
setEditPane("zone");
|
||||
handleNewPolygon("zone");
|
||||
@ -586,7 +586,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new motion mask"
|
||||
aria-label={t("masksAndZones.motionMasks.add")}
|
||||
onClick={() => {
|
||||
setEditPane("motion_mask");
|
||||
handleNewPolygon("motion_mask");
|
||||
@ -654,7 +654,7 @@ export default function MasksAndZonesView({
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
|
||||
aria-label="Add a new object mask"
|
||||
aria-label={t("masksAndZones.objectMasks.add")}
|
||||
onClick={() => {
|
||||
setEditPane("object_mask");
|
||||
handleNewPolygon("object_mask");
|
||||
|
||||
@ -295,7 +295,7 @@ export default function MotionTunerView({
|
||||
<div className="flex flex-row gap-2 pt-5">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Reset"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.reset", { ns: "common" })}
|
||||
@ -304,7 +304,7 @@ export default function MotionTunerView({
|
||||
variant="select"
|
||||
disabled={!changedValue || isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -266,9 +266,15 @@ export default function NotificationView({
|
||||
});
|
||||
updateConfig();
|
||||
} else {
|
||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
toast.error(
|
||||
t("toast.save.error", {
|
||||
errorMessage: res.statusText,
|
||||
ns: "common",
|
||||
}),
|
||||
{
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -276,7 +282,7 @@ export default function NotificationView({
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(`Failed to save config changes: ${errorMessage}`, {
|
||||
toast.error(t("toast.save.error", { errorMessage, ns: "common" }), {
|
||||
position: "top-center",
|
||||
});
|
||||
})
|
||||
@ -284,7 +290,7 @@ export default function NotificationView({
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[updateConfig, setIsLoading, allCameras],
|
||||
[updateConfig, setIsLoading, allCameras, t],
|
||||
);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
@ -474,7 +480,7 @@ export default function NotificationView({
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[50%]">
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label="Cancel"
|
||||
aria-label={t("button.cancel", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
>
|
||||
@ -484,7 +490,7 @@ export default function NotificationView({
|
||||
variant="select"
|
||||
disabled={isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -295,14 +295,18 @@ export default function ExploreSettingsView({
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
|
||||
<Button className="flex flex-1" aria-label="Reset" onClick={onCancel}>
|
||||
<Button
|
||||
className="flex flex-1"
|
||||
aria-label={t("button.reset", { ns: "common" })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t("button.reset", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="select"
|
||||
disabled={!changedValue || isLoading}
|
||||
className="flex flex-1"
|
||||
aria-label="Save"
|
||||
aria-label={t("button.save", { ns: "common" })}
|
||||
onClick={saveToConfig}
|
||||
>
|
||||
{isLoading ? (
|
||||
|
||||
@ -142,7 +142,7 @@ export default function UiSettingsView() {
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Clear all saved layouts"
|
||||
aria-label={t("general.storedLayouts.clearAll")}
|
||||
onClick={clearStoredLayouts}
|
||||
>
|
||||
{t("general.storedLayouts.clearAll")}
|
||||
@ -159,7 +159,7 @@ export default function UiSettingsView() {
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Clear all group streaming settings"
|
||||
aria-label={t("general.cameraGroupStreaming.clearAll")}
|
||||
onClick={clearStreamingSettings}
|
||||
>
|
||||
{t("general.cameraGroupStreaming.clearAll")}
|
||||
|
||||
@ -542,7 +542,7 @@ export default function GeneralMetrics({
|
||||
{canGetGpuInfo && (
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
aria-label="Hardware information"
|
||||
aria-label={t("general.hardwareInfo.title")}
|
||||
size="sm"
|
||||
onClick={() => setShowVainfo(true)}
|
||||
>
|
||||
|
||||
@ -87,11 +87,15 @@ export default function StorageMetrics({
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="focus:outline-none"
|
||||
aria-label="Unused Storage Information"
|
||||
aria-label={t(
|
||||
"storage.cameraStorage.unusedStorageInformation",
|
||||
)}
|
||||
>
|
||||
<CiCircleAlert
|
||||
className="size-5"
|
||||
aria-label="Unused Storage Information"
|
||||
aria-label={t(
|
||||
"storage.cameraStorage.unusedStorageInformation",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
@ -107,7 +111,9 @@ export default function StorageMetrics({
|
||||
/>
|
||||
{earliestDate && (
|
||||
<div className="mt-2 text-xs text-primary-variant">
|
||||
<span className="font-medium">Earliest recording available:</span>{" "}
|
||||
<span className="font-medium">
|
||||
{t("storage.recordings.earliestRecording")}
|
||||
</span>{" "}
|
||||
{formatUnixTimestampToDateTime(earliestDate, {
|
||||
timezone: timezone,
|
||||
strftime_fmt:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user