mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-19 01:17:06 +03:00
Add more translation
This commit is contained in:
parent
7476fa1300
commit
08d0215673
@ -109,6 +109,20 @@
|
|||||||
"audio.fire_alarm": "Fire alarm",
|
"audio.fire_alarm": "Fire alarm",
|
||||||
|
|
||||||
"ui.time.justNow": "Just now",
|
"ui.time.justNow": "Just now",
|
||||||
|
"ui.time.today": "Today",
|
||||||
|
"ui.time.yesterday": "Yesterday",
|
||||||
|
"ui.time.last7": "Last 7 days",
|
||||||
|
"ui.time.last14": "Last 14 days",
|
||||||
|
"ui.time.last30": "Last 30 days",
|
||||||
|
"ui.time.thisWeek": "This Week",
|
||||||
|
"ui.time.lastWeek": "Last Week",
|
||||||
|
"ui.time.thisMonth": "This Month",
|
||||||
|
"ui.time.lastMonth": "Last Month",
|
||||||
|
"ui.time.formattedTimestamp": "%b %-d, %I:%M:%S %p",
|
||||||
|
"ui.time.formattedTimestamp.24hour": "%b %-d, %H:%M:%S",
|
||||||
|
|
||||||
|
"ui.iconPicker.selectIcon": "Select an icon",
|
||||||
|
"ui.iconPicker.search.placeholder": "Search for an icon...",
|
||||||
|
|
||||||
"ui.dialog.restart.title": "Are you sure you want to restart Frigate?",
|
"ui.dialog.restart.title": "Are you sure you want to restart Frigate?",
|
||||||
"ui.dialog.restart.button": "Restart",
|
"ui.dialog.restart.button": "Restart",
|
||||||
@ -116,6 +130,20 @@
|
|||||||
"ui.dialog.restart.restarting.content": "This page will reload in {{countdown}} seconds.",
|
"ui.dialog.restart.restarting.content": "This page will reload in {{countdown}} seconds.",
|
||||||
"ui.dialog.restart.restarting.button": "Force Reload Now",
|
"ui.dialog.restart.restarting.button": "Force Reload Now",
|
||||||
|
|
||||||
|
"ui.dialog.export.time.fromTimeline": "Select from Timeline",
|
||||||
|
"ui.dialog.export.time.lastHour_one": "Last Hour",
|
||||||
|
"ui.dialog.export.time.lastHour_other": "Last {{count}} Hours",
|
||||||
|
"ui.dialog.export.time.custom": "Custom",
|
||||||
|
"ui.dialog.export.name.placeholder": "Name the Export",
|
||||||
|
"ui.dialog.export.select": "Select",
|
||||||
|
"ui.dialog.export.export": "Export",
|
||||||
|
"ui.dialog.export.toast.success": "Successfully started export. View the file in the /exports folder.",
|
||||||
|
"ui.dialog.export.toast.error.failed": "Failed to start export: {{error}}",
|
||||||
|
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "End time must be after start time",
|
||||||
|
"ui.dialog.export.toast.error.noVaildTimeSelected": "No valid time range selected",
|
||||||
|
"ui.dialog.export.fromTimeline.saveExport": "Save Export",
|
||||||
|
"ui.dialog.export.fromTimeline.previewExport": "Preview Export",
|
||||||
|
|
||||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
|
"ui.stats.ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
|
||||||
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
|
"ui.stats.detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
|
||||||
"ui.stats.healthy": "System is healthy",
|
"ui.stats.healthy": "System is healthy",
|
||||||
@ -209,6 +237,23 @@
|
|||||||
"ui.menu.user.anonymous": "anonymous",
|
"ui.menu.user.anonymous": "anonymous",
|
||||||
"ui.menu.user.logout": "Logout",
|
"ui.menu.user.logout": "Logout",
|
||||||
|
|
||||||
|
"ui.cameraGroup": "Camera Groups",
|
||||||
|
"ui.cameraGroup.add": "添加摄像头组",
|
||||||
|
"ui.cameraGroup.edit": "Edit camera groups",
|
||||||
|
"ui.cameraGroup.delete.confirm": "Confirm Delete",
|
||||||
|
"ui.cameraGroup.delete.confirm.desc": "Are you sure you want to delete the camera group <em>{{name}}</em>?",
|
||||||
|
"ui.cameraGroup.name": "Name",
|
||||||
|
"ui.cameraGroup.name.placeholder": "Enter a name...",
|
||||||
|
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "Camera group name must be at least 2 characters.",
|
||||||
|
"ui.cameraGroup.name.errorMessage.exists": "Camera group name already exists.",
|
||||||
|
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "Camera group name must not contain a period.",
|
||||||
|
"ui.cameraGroup.name.errorMessage.invalid": "Invalid camera group name.",
|
||||||
|
"ui.cameraGroup.cameras": "Cameras",
|
||||||
|
"ui.cameraGroup.cameras.desc": "Select cameras for this group.",
|
||||||
|
"ui.cameraGroup.icon": "Icon",
|
||||||
|
"ui.cameraGroup.success": "Camera group ({{name}}) has been saved.",
|
||||||
|
"ui.cameraGroup.toast.error": "Failed to save config changes: {{error}}",
|
||||||
|
|
||||||
"ui.eventView.alerts": "Alerts",
|
"ui.eventView.alerts": "Alerts",
|
||||||
"ui.eventView.detections": "Detections",
|
"ui.eventView.detections": "Detections",
|
||||||
"ui.eventView.motion": "Motion",
|
"ui.eventView.motion": "Motion",
|
||||||
@ -216,9 +261,33 @@
|
|||||||
"ui.eventView.empty.alert": "There are no alerts to review",
|
"ui.eventView.empty.alert": "There are no alerts to review",
|
||||||
"ui.eventView.empty.detection": "There are no detections to review",
|
"ui.eventView.empty.detection": "There are no detections to review",
|
||||||
|
|
||||||
"ui.reviewFilter.filter": "Filter",
|
"ui.filter": "Filter",
|
||||||
"ui.reviewFilter.filter.allLabels": "All Labels",
|
"ui.filter.allLabels": "All Labels",
|
||||||
"ui.reviewFilter.filter.allZones": "All Zones",
|
"ui.filter.allLabels.short": "Labels",
|
||||||
|
"ui.filter.countLabels": "{{count}} Labels",
|
||||||
|
"ui.filter.allZones": "All Zones",
|
||||||
|
"ui.filter.allZones.short": "Zones",
|
||||||
|
"ui.filter.allDates": "All Dates",
|
||||||
|
"ui.filter.allDates.short": "Dates",
|
||||||
|
"ui.filter.more": "More Filters",
|
||||||
|
"ui.filter.timeRange": "Time Range",
|
||||||
|
"ui.filter.zones": "Zones",
|
||||||
|
"ui.filter.subLabels": "Sub Labels",
|
||||||
|
"ui.filter.allSubLabels": "All Sub Labels",
|
||||||
|
"ui.filter.score": "Score",
|
||||||
|
"ui.filter.features": "Features",
|
||||||
|
"ui.filter.features.hasSnapshot": "Has a snapshot",
|
||||||
|
"ui.filter.features.hasVideoClip": "Has a video clip",
|
||||||
|
"ui.filter.features.submittedToFrigatePlus": "Submitted to Frigate+",
|
||||||
|
"ui.filter.features.submittedToFrigatePlus.tips": "You must first filter on tracked objects that have a snapshot.<br /><br />Tracked objects without a snapshot cannot be submitted to Frigate+.",
|
||||||
|
"ui.filter.sort": "Sort",
|
||||||
|
"ui.filter.sort.dateAsc": "Date (Ascending)",
|
||||||
|
"ui.filter.sort.dateDesc": "Date (Descending)",
|
||||||
|
"ui.filter.sort.scoreAsc": "Object Score (Ascending)",
|
||||||
|
"ui.filter.sort.scoreDesc": "Object Score (Descending)",
|
||||||
|
"ui.filter.sort.relevance": "Relevance",
|
||||||
|
"ui.filter.allCameras": "All Cameras",
|
||||||
|
"ui.filter.allCameras.short": "Cameras",
|
||||||
|
|
||||||
"ui.reviewFilter.showReviewed": "Show Reviewed",
|
"ui.reviewFilter.showReviewed": "Show Reviewed",
|
||||||
|
|
||||||
@ -236,8 +305,13 @@
|
|||||||
"ui.pictureInPicture": "Picture in Picture",
|
"ui.pictureInPicture": "Picture in Picture",
|
||||||
"ui.on": "ON",
|
"ui.on": "ON",
|
||||||
"ui.off": "OFF",
|
"ui.off": "OFF",
|
||||||
|
"ui.edit": "Edit",
|
||||||
"ui.delete": "Delete",
|
"ui.delete": "Delete",
|
||||||
|
"ui.yes": "Yes",
|
||||||
|
"ui.no": "No",
|
||||||
|
|
||||||
|
"ui.live.documentTitle": "Live - Frigate",
|
||||||
|
"ui.live.documentTitle.withCamera": "{{camera}} - Live - Frigate",
|
||||||
"ui.live.twoWayTalk.enable": "Enable Two Way Talk",
|
"ui.live.twoWayTalk.enable": "Enable Two Way Talk",
|
||||||
"ui.live.twoWayTalk.disable": "Disable Two Way Talk",
|
"ui.live.twoWayTalk.disable": "Disable Two Way Talk",
|
||||||
"ui.live.cameraAudio.enable": "Enable Camera Audio",
|
"ui.live.cameraAudio.enable": "Enable Camera Audio",
|
||||||
@ -261,10 +335,31 @@
|
|||||||
"ui.live.autotracking.enable": "Enable Autotracking",
|
"ui.live.autotracking.enable": "Enable Autotracking",
|
||||||
"ui.live.autotracking.disable": "Disable Autotracking",
|
"ui.live.autotracking.disable": "Disable Autotracking",
|
||||||
|
|
||||||
|
"ui.review.timeline": "Timeline",
|
||||||
|
"ui.review.events": "Events",
|
||||||
|
"ui.review.events.noFoundForTimePeriod": "No events found for this time period.",
|
||||||
|
"ui.review.documentTitle": "Review - Frigate",
|
||||||
|
"ui.review.recordings.documentTitle": "Recordings - Frigate",
|
||||||
|
|
||||||
|
"ui.player.noRecordingsFoundForThisTime": "No recordings found for this time",
|
||||||
|
"ui.player.noPreviewFound": "No Preview Found",
|
||||||
|
"ui.player.noPreviewFoundFor": "No Preview Found for {{cameraName}}",
|
||||||
|
|
||||||
"ui.calendarFilter.last24Hours": "Last 24 Hours",
|
"ui.calendarFilter.last24Hours": "Last 24 Hours",
|
||||||
|
|
||||||
|
"ui.searchView.noTrackedObjects": "No Tracked Objects Found",
|
||||||
|
"ui.searchView.settings": "Settings",
|
||||||
|
"ui.searchView.settings.defaultView": "Default View",
|
||||||
|
"ui.searchView.settings.defaultView.desc": "When no filters are selected, display a summary of the most recent tracked objects per label, or display an unfiltered grid.",
|
||||||
|
"ui.searchView.settings.defaultView.summary": "Summary",
|
||||||
|
"ui.searchView.settings.defaultView.unfilteredGrid": "Unfiltered Grid",
|
||||||
|
"ui.searchView.settings.gridColumns": "Grid Columns",
|
||||||
|
"ui.searchView.settings.gridColumns.desc": "Select the number of columns in the grid view.",
|
||||||
|
"ui.searchView.settings.searchSource": "Search Source",
|
||||||
|
"ui.searchView.settings.searchSource.desc": "Choose whether to search the thumbnails or descriptions of your tracked objects.",
|
||||||
|
|
||||||
"ui.settingView.menu.uiSettings": "UI Settings",
|
"ui.settingView.menu.uiSettings": "UI Settings",
|
||||||
"ui.settingView.menu.searchSettings": "Search Settings",
|
"ui.settingView.menu.exploreSettings": "Explore Settings",
|
||||||
"ui.settingView.menu.cameraSettings": "Camera Settings",
|
"ui.settingView.menu.cameraSettings": "Camera Settings",
|
||||||
"ui.settingView.menu.masksAndZones": "Masks / Zones",
|
"ui.settingView.menu.masksAndZones": "Masks / Zones",
|
||||||
"ui.settingView.menu.motionTuner": "Motion Tuner",
|
"ui.settingView.menu.motionTuner": "Motion Tuner",
|
||||||
@ -290,18 +385,18 @@
|
|||||||
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday",
|
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "Sunday",
|
||||||
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday",
|
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "Monday",
|
||||||
|
|
||||||
"ui.settingView.searchSettings": "Search Settings",
|
"ui.settingView.exploreSettings": "Explore Settings",
|
||||||
"ui.settingView.searchSettings.semanticSearch": "Semantic Search",
|
"ui.settingView.exploreSettings.semanticSearch": "Semantic Search",
|
||||||
"ui.settingView.searchSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
"ui.settingView.exploreSettings.semanticSearch.desc": "Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one.",
|
||||||
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
|
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "Read the Documentation",
|
||||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
|
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "Re-Index On Startup",
|
||||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
|
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "Re-indexing will reprocess all thumbnails and descriptions (if enabled) and apply the embeddings on each startup. <em>Don't forget to disable the option after restarting!</em>",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize": "Model Size",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize": "Model Size",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "The size of the model used for semantic search embeddings.",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "small",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "small",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "large",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "large",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "Using <em>small</em> employs a quantized version of the model that uses less RAM and runs faster on CPU with a very negligible difference in embedding quality.",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "Using <em>large</em> employs the full Jina model and will automatically run on the GPU if applicable.",
|
||||||
|
|
||||||
"ui.settingView.cameraSettings": "Camera Settings",
|
"ui.settingView.cameraSettings": "Camera Settings",
|
||||||
"ui.settingView.cameraSettings.reviewClassification": "Review Classification",
|
"ui.settingView.cameraSettings.reviewClassification": "Review Classification",
|
||||||
@ -319,6 +414,7 @@
|
|||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings": "Masks / Zones",
|
"ui.settingView.masksAndZonesSettings": "Masks / Zones",
|
||||||
"ui.settingView.masksAndZonesSettings.zone": "Zones",
|
"ui.settingView.masksAndZonesSettings.zone": "Zones",
|
||||||
|
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "Edit Zone - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.desc": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
|
"ui.settingView.masksAndZonesSettings.zone.desc": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "Documentation",
|
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "Documentation",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
|
"ui.settingView.masksAndZonesSettings.zone.add": "Add Zone",
|
||||||
@ -338,6 +434,7 @@
|
|||||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
|
"ui.settingView.masksAndZonesSettings.zone.allObjects": "All Objects",
|
||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
|
"ui.settingView.masksAndZonesSettings.motionMasks": "Motion Mask",
|
||||||
|
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "Edit Motion Mask - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.",
|
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "Motion masks are used to prevent unwanted types of motion from triggering detection. Over masking will make it more difficult for objects to be tracked.",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "Documentation",
|
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "Documentation",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask",
|
"ui.settingView.masksAndZonesSettings.motionMasks.add": "New Motion Mask",
|
||||||
@ -351,8 +448,8 @@
|
|||||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead.",
|
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.tips": "Motion masks do not prevent objects from being detected. You should use a required zone instead.",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation": "Read the documentation",
|
"ui.settingView.masksAndZonesSettings.motionMasks.polygonAreaTooLarge.documentation": "Read the documentation",
|
||||||
|
|
||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks": "Object Masks",
|
"ui.settingView.masksAndZonesSettings.objectMasks": "Object Masks",
|
||||||
|
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "Edit Object Mask - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "Object filter masks are used to filter out false positives for a given object type based on location.",
|
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "Object filter masks are used to filter out false positives for a given object type based on location.",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "Documentation",
|
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "Documentation",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
|
"ui.settingView.masksAndZonesSettings.objectMasks.add": "Add Object Mask",
|
||||||
@ -423,6 +520,11 @@
|
|||||||
"ui.configEditorView.configEditor": "Config Editor",
|
"ui.configEditorView.configEditor": "Config Editor",
|
||||||
"ui.configEditorView.copyConfig": "Copy Config",
|
"ui.configEditorView.copyConfig": "Copy Config",
|
||||||
"ui.configEditorView.saveAndRestart": "Save & Restart",
|
"ui.configEditorView.saveAndRestart": "Save & Restart",
|
||||||
"ui.configEditorView.saveOnly": "Save Only"
|
"ui.configEditorView.saveOnly": "Save Only",
|
||||||
|
|
||||||
|
"ui.exportView.documentTitle": "Export - Frigate",
|
||||||
|
"ui.exportView.search": "Search",
|
||||||
|
"ui.exportView.noExports": "No exports found",
|
||||||
|
"ui.exportView.deleteExport": "Delete Export",
|
||||||
|
"ui.exportView.deleteExport.desc": "Are you sure you want to delete {{exportName}}?"
|
||||||
}
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"object.person": "人",
|
"object.person": "人",
|
||||||
"object.bicycle": "自行车",
|
"object.bicycle": "自行车",
|
||||||
"object.car": "汽车",
|
"object.car": "汽车",
|
||||||
@ -110,6 +109,22 @@
|
|||||||
"audio.fire_alarm": "火灾警报器",
|
"audio.fire_alarm": "火灾警报器",
|
||||||
|
|
||||||
"ui.time.justNow": "刚才",
|
"ui.time.justNow": "刚才",
|
||||||
|
"ui.time.today": "今天",
|
||||||
|
"ui.time.yesterday": "昨天",
|
||||||
|
"ui.time.last7": "最后 7 天",
|
||||||
|
"ui.time.last14": "最后 14 天",
|
||||||
|
"ui.time.last30": "最后 30 天",
|
||||||
|
"ui.time.thisWeek": "本周",
|
||||||
|
"ui.time.lastWeek": "上个周",
|
||||||
|
"ui.time.thisMonth": "本月",
|
||||||
|
"ui.time.lastMonth": "上个月",
|
||||||
|
"ui.time.pm": "上午",
|
||||||
|
"ui.time.am": "下午",
|
||||||
|
"ui.time.formattedTimestamp": "%m月%-d日 %I:%M:%S %p",
|
||||||
|
"ui.time.formattedTimestamp.24hour": "%m月%-d日 %H:%M:%S",
|
||||||
|
|
||||||
|
"ui.iconPicker.selectIcon": "选择图标",
|
||||||
|
"ui.iconPicker.search.placeholder": "搜索图标...",
|
||||||
|
|
||||||
"ui.dialog.restart.title": "你确定要重启 Frigate?",
|
"ui.dialog.restart.title": "你确定要重启 Frigate?",
|
||||||
"ui.dialog.restart.button": "重启",
|
"ui.dialog.restart.button": "重启",
|
||||||
@ -117,6 +132,21 @@
|
|||||||
"ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
|
"ui.dialog.restart.restarting.content": "该页面将会在 {{countdown}} 秒后自动刷新。",
|
||||||
"ui.dialog.restart.restarting.button": "强制刷新",
|
"ui.dialog.restart.restarting.button": "强制刷新",
|
||||||
|
|
||||||
|
"ui.dialog.export.time.fromTimeline": "从时间线选择",
|
||||||
|
"ui.dialog.export.time.lastHour_one": "最后1小时",
|
||||||
|
"ui.dialog.export.time.lastHour_other": "最后 {{count}} 小时",
|
||||||
|
"ui.dialog.export.time.custom": "自定义",
|
||||||
|
"ui.dialog.export.name.placeholder": "导出项目的名字",
|
||||||
|
"ui.dialog.export.select": "选择",
|
||||||
|
"ui.dialog.export.export": "导出",
|
||||||
|
"ui.dialog.export.toast.success": "导出成功。进入 /exports 目录查看文件。",
|
||||||
|
"ui.dialog.export.toast.error.failed": "导出失败:{{error}}",
|
||||||
|
"ui.dialog.export.toast.error.endTimeMustAfterStartTime": "结束时间必须在开始时间之后",
|
||||||
|
"ui.dialog.export.toast.error.noVaildTimeSelected": "未选择有效的时间范围",
|
||||||
|
"ui.dialog.export.fromTimeline.saveExport": "保存导出",
|
||||||
|
"ui.dialog.export.fromTimeline.previewExport": "预览导出",
|
||||||
|
|
||||||
|
|
||||||
"ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%)",
|
"ui.stats.ffmpegHighCpuUsage": "{{camera}} 的 FFMPEG CPU 使用率较高({{ffmpegAvg}}%)",
|
||||||
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%)",
|
"ui.stats.detectHighCpuUsage": "{{camera}} 的 探测 CPU 使用率较高({{detectAvg}}%)",
|
||||||
"ui.stats.healthy": "系统运行正常",
|
"ui.stats.healthy": "系统运行正常",
|
||||||
@ -203,13 +233,31 @@
|
|||||||
"ui.menu.live": "实时监控",
|
"ui.menu.live": "实时监控",
|
||||||
"ui.menu.live.allCameras": "所有摄像头",
|
"ui.menu.live.allCameras": "所有摄像头",
|
||||||
"ui.menu.review": "回放",
|
"ui.menu.review": "回放",
|
||||||
"ui.menu.explore": "Explore",
|
"ui.menu.explore": "探测",
|
||||||
"ui.menu.export": "导出",
|
"ui.menu.export": "导出",
|
||||||
"ui.menu.uiPlayground": "UI Playground",
|
"ui.menu.uiPlayground": "UI Playground",
|
||||||
"ui.menu.user.current": "当前用户:{{user}}",
|
"ui.menu.user.current": "当前用户:{{user}}",
|
||||||
"ui.menu.user.anonymous": "匿名",
|
"ui.menu.user.anonymous": "匿名",
|
||||||
"ui.menu.user.logout": "登出",
|
"ui.menu.user.logout": "登出",
|
||||||
|
|
||||||
|
"ui.cameraGroup": "摄像头组",
|
||||||
|
"ui.cameraGroup.add": "添加摄像头组",
|
||||||
|
"ui.cameraGroup.edit": "编辑摄像头组",
|
||||||
|
"ui.cameraGroup.edit.desc": "编辑摄像头组",
|
||||||
|
"ui.cameraGroup.delete.confirm": "确认删除",
|
||||||
|
"ui.cameraGroup.delete.confirm.desc": "你确定要删除摄像头组 <strong>{{name}}</strong> 吗?",
|
||||||
|
"ui.cameraGroup.name": "名称",
|
||||||
|
"ui.cameraGroup.name.placeholder": "请输入名称",
|
||||||
|
"ui.cameraGroup.name.errorMessage.mustLeastCharacters": "摄像头组的名称必须至少有 2 个字符。",
|
||||||
|
"ui.cameraGroup.name.errorMessage.exists": "摄像头组名称已存在。",
|
||||||
|
"ui.cameraGroup.name.errorMessage.nameMustNotPeriod": "摄像头组名称不能包含英文句号(.)。",
|
||||||
|
"ui.cameraGroup.name.errorMessage.invalid": "无效的摄像头组名称。",
|
||||||
|
"ui.cameraGroup.cameras": "摄像头",
|
||||||
|
"ui.cameraGroup.cameras.desc": "选择添加至该组的摄像头。",
|
||||||
|
"ui.cameraGroup.icon": "图标",
|
||||||
|
"ui.cameraGroup.toast.success": "摄像头组({{name}})保存成功。",
|
||||||
|
"ui.cameraGroup.toast.error": "保存设置失败: {{error}}",
|
||||||
|
|
||||||
"ui.eventView.alerts": "警告",
|
"ui.eventView.alerts": "警告",
|
||||||
"ui.eventView.detections": "检测",
|
"ui.eventView.detections": "检测",
|
||||||
"ui.eventView.motion": "运动",
|
"ui.eventView.motion": "运动",
|
||||||
@ -217,9 +265,33 @@
|
|||||||
"ui.eventView.empty.alert": "还没有“警告”类回放",
|
"ui.eventView.empty.alert": "还没有“警告”类回放",
|
||||||
"ui.eventView.empty.detection": "还没有“探测”类回放",
|
"ui.eventView.empty.detection": "还没有“探测”类回放",
|
||||||
|
|
||||||
"ui.reviewFilter.filter": "过滤器",
|
"ui.filter": "过滤器",
|
||||||
"ui.reviewFilter.filter.allLabels": "所有标签",
|
"ui.filter.allLabels": "所有标签",
|
||||||
"ui.reviewFilter.filter.allZones": "所有区域",
|
"ui.filter.allLabels.short": "标签",
|
||||||
|
"ui.filter.countLabels": "{{count}} 个标签",
|
||||||
|
"ui.filter.allZones": "所有区域",
|
||||||
|
"ui.filter.allZones.short": "区域",
|
||||||
|
"ui.filter.allDates": "所有日期",
|
||||||
|
"ui.filter.allDates.short": "日期",
|
||||||
|
"ui.filter.more": "更多筛选项",
|
||||||
|
"ui.filter.timeRange": "时间范围",
|
||||||
|
"ui.filter.zones": "区域",
|
||||||
|
"ui.filter.subLabels": "子标签",
|
||||||
|
"ui.filter.allSubLabels": "所有子标签",
|
||||||
|
"ui.filter.score": "分值",
|
||||||
|
"ui.filter.features": "特性",
|
||||||
|
"ui.filter.features.hasSnapshot": "包含快照",
|
||||||
|
"ui.filter.features.hasVideoClip": "包含视频片段",
|
||||||
|
"ui.filter.features.submittedToFrigatePlus": "提交至 Frigate+",
|
||||||
|
"ui.filter.features.submittedToFrigatePlus.tips": "你必须要先筛选具有快照的探测对象。<br /><br />没有快照的跟踪对象无法提交至 Frigate+.",
|
||||||
|
"ui.filter.sort": "排序",
|
||||||
|
"ui.filter.sort.dateAsc": "日期 (正序)",
|
||||||
|
"ui.filter.sort.dateDesc": "日期 (倒序)",
|
||||||
|
"ui.filter.sort.scoreAsc": "对象分值 (正序)",
|
||||||
|
"ui.filter.sort.scoreDesc": "对象分值 (倒序)",
|
||||||
|
"ui.filter.sort.relevance": "关联性",
|
||||||
|
"ui.filter.allCameras": "所有摄像头",
|
||||||
|
"ui.filter.allCameras.short": "摄像头",
|
||||||
|
|
||||||
"ui.reviewFilter.showReviewed": "显示已查看的项目",
|
"ui.reviewFilter.showReviewed": "显示已查看的项目",
|
||||||
|
|
||||||
@ -238,8 +310,13 @@
|
|||||||
"ui.pictureInPicture": "画中画",
|
"ui.pictureInPicture": "画中画",
|
||||||
"ui.on": "开",
|
"ui.on": "开",
|
||||||
"ui.off": "关",
|
"ui.off": "关",
|
||||||
|
"ui.edit": "编辑",
|
||||||
"ui.delete": "删除",
|
"ui.delete": "删除",
|
||||||
|
"ui.yes": "是",
|
||||||
|
"ui.no": "否",
|
||||||
|
|
||||||
|
"ui.live.documentTitle": "实时监控 - Frigate",
|
||||||
|
"ui.live.documentTitle.withCamera": "{{camera}} - 实时监控 - Frigate",
|
||||||
"ui.live.twoWayTalk.enable": "开启双向对话",
|
"ui.live.twoWayTalk.enable": "开启双向对话",
|
||||||
"ui.live.twoWayTalk.disable": "关闭双向对话",
|
"ui.live.twoWayTalk.disable": "关闭双向对话",
|
||||||
"ui.live.cameraAudio.enable": "开启摄像头音频",
|
"ui.live.cameraAudio.enable": "开启摄像头音频",
|
||||||
@ -263,10 +340,31 @@
|
|||||||
"ui.live.autotracking.enable": "启用自动追踪",
|
"ui.live.autotracking.enable": "启用自动追踪",
|
||||||
"ui.live.autotracking.disable": "关闭自动追踪",
|
"ui.live.autotracking.disable": "关闭自动追踪",
|
||||||
|
|
||||||
|
"ui.review.timeline": "时间线",
|
||||||
|
"ui.review.events": "事件",
|
||||||
|
"ui.review.events.noFoundForTimePeriod": "未找到该时间段的事件。",
|
||||||
|
"ui.review.documentTitle": "预览 - Frigate",
|
||||||
|
"ui.review.recordings.documentTitle": "回放 - Frigate",
|
||||||
|
|
||||||
|
"ui.player.noRecordingsFoundForThisTime": "找不到此次录制",
|
||||||
|
"ui.player.noPreviewFound": "没有找到预览",
|
||||||
|
"ui.player.noPreviewFoundFor": "没有在 {{cameraName}} 下找到预览",
|
||||||
|
|
||||||
"ui.calendarFilter.last24Hours": "过去24小时",
|
"ui.calendarFilter.last24Hours": "过去24小时",
|
||||||
|
|
||||||
|
"ui.searchView.noTrackedObjects": "找不到探测的对象",
|
||||||
|
"ui.searchView.settings": "设置",
|
||||||
|
"ui.searchView.settings.defaultView": "默认视图",
|
||||||
|
"ui.searchView.settings.defaultView.desc": "当未选择任何过滤器时,显示每个标签最近跟踪对象的摘要,或显示未过滤的网格。",
|
||||||
|
"ui.searchView.settings.defaultView.summary": "摘要",
|
||||||
|
"ui.searchView.settings.defaultView.unfilteredGrid": "未过滤网格",
|
||||||
|
"ui.searchView.settings.gridColumns": "网格列数",
|
||||||
|
"ui.searchView.settings.gridColumns.desc": "选择网格视图中的列数。",
|
||||||
|
"ui.searchView.settings.searchSource": "搜索源",
|
||||||
|
"ui.searchView.settings.searchSource.desc": "选择是搜索缩略图还是跟踪对象的描述。",
|
||||||
|
|
||||||
"ui.settingView.menu.uiSettings": "界面设置",
|
"ui.settingView.menu.uiSettings": "界面设置",
|
||||||
"ui.settingView.menu.searchSettings": "搜索设置",
|
"ui.settingView.menu.exploreSettings": "搜索设置",
|
||||||
"ui.settingView.menu.cameraSettings": "摄像头设置",
|
"ui.settingView.menu.cameraSettings": "摄像头设置",
|
||||||
"ui.settingView.menu.masksAndZones": "屏罩 / 区域",
|
"ui.settingView.menu.masksAndZones": "屏罩 / 区域",
|
||||||
"ui.settingView.menu.motionTuner": "运动调整器",
|
"ui.settingView.menu.motionTuner": "运动调整器",
|
||||||
@ -292,18 +390,18 @@
|
|||||||
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天",
|
"ui.settingView.generalSettings.calendar.firstWeekday.sunday": "星期天",
|
||||||
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一",
|
"ui.settingView.generalSettings.calendar.firstWeekday.monday": "星期一",
|
||||||
|
|
||||||
"ui.settingView.searchSettings": "搜索设置",
|
"ui.settingView.exploreSettings": "探测设置",
|
||||||
"ui.settingView.searchSettings.semanticSearch": "语义搜索",
|
"ui.settingView.exploreSettings.semanticSearch": "语义搜索",
|
||||||
"ui.settingView.searchSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
|
"ui.settingView.exploreSettings.semanticSearch.desc": "Frigate的语义搜索能够让你使用自然语言根据图像本身、自定义的文本描述或自动生成的描述来搜索视频。",
|
||||||
"ui.settingView.searchSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
|
"ui.settingView.exploreSettings.semanticSearch.readTheDocumentation": "阅读文档(英文)",
|
||||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
|
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup": "启动时重新索引",
|
||||||
"ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
|
"ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc": "每次启动将重新索引并重新处理所有缩略图和描述。<em>关闭该设置后不要忘记重启!</em>",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize": "模型大小",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize": "模型大小",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.desc": "用于语义搜索的语言模型大小",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small": "小",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.small": "小",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large": "大",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.large": "大",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc": "使用 <strong>小</strong>模型。该模型将使用较少的内存,在CPU上也能较快的运行。质量较好。",
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。",
|
"ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc": "使用 <strong>大</strong>模型。该模型采用了完整的Jina模型,并在适用的情况下使用GPU。",
|
||||||
|
|
||||||
"ui.settingView.cameraSettings": "摄像头设置",
|
"ui.settingView.cameraSettings": "摄像头设置",
|
||||||
"ui.settingView.cameraSettings.reviewClassification": "预览分级",
|
"ui.settingView.cameraSettings.reviewClassification": "预览分级",
|
||||||
@ -321,6 +419,7 @@
|
|||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
|
"ui.settingView.masksAndZonesSettings": "屏罩 / 区域",
|
||||||
"ui.settingView.masksAndZonesSettings.zone": "区域",
|
"ui.settingView.masksAndZonesSettings.zone": "区域",
|
||||||
|
"ui.settingView.masksAndZonesSettings.zone.documentTitle": "编辑区域 - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
|
"ui.settingView.masksAndZonesSettings.zone.desc": "该功能允许你定义特定区域,以便你可以确定特定对象是否在该区域内。",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
|
"ui.settingView.masksAndZonesSettings.zone.desc.documentation": "文档(英文)",
|
||||||
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
|
"ui.settingView.masksAndZonesSettings.zone.add": "添加区域",
|
||||||
@ -340,6 +439,7 @@
|
|||||||
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
|
"ui.settingView.masksAndZonesSettings.zone.allObjects": "所有对象",
|
||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
|
"ui.settingView.masksAndZonesSettings.motionMasks": "运动遮罩",
|
||||||
|
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle": "编辑运动遮罩 - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
"ui.settingView.masksAndZonesSettings.motionMasks.desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
|
"ui.settingView.masksAndZonesSettings.motionMasks.desc.documentation": "文档(英文)",
|
||||||
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
|
"ui.settingView.masksAndZonesSettings.motionMasks.add": "添加运动遮罩",
|
||||||
@ -355,6 +455,7 @@
|
|||||||
|
|
||||||
|
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
|
"ui.settingView.masksAndZonesSettings.objectMasks": "对象遮罩",
|
||||||
|
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle": "编辑对象遮罩 - Frigate",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
|
"ui.settingView.masksAndZonesSettings.objectMasks.desc": "对象过滤器用于防止特定位置的指定对象被误报。",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
|
"ui.settingView.masksAndZonesSettings.objectMasks.documentation": "文档(英文)",
|
||||||
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
|
"ui.settingView.masksAndZonesSettings.objectMasks.add": "添加对象遮罩",
|
||||||
@ -425,5 +526,12 @@
|
|||||||
"ui.configEditorView.configEditor": "配置编辑器",
|
"ui.configEditorView.configEditor": "配置编辑器",
|
||||||
"ui.configEditorView.copyConfig": "复制配置",
|
"ui.configEditorView.copyConfig": "复制配置",
|
||||||
"ui.configEditorView.saveAndRestart": "保存并重启",
|
"ui.configEditorView.saveAndRestart": "保存并重启",
|
||||||
"ui.configEditorView.saveOnly": "只保存"
|
"ui.configEditorView.saveOnly": "只保存",
|
||||||
|
|
||||||
|
"ui.exportView.documentTitle": "导出 - Frigate",
|
||||||
|
"ui.exportView.search": "搜索",
|
||||||
|
"ui.exportView.noExports": "没有找到导出的项目",
|
||||||
|
"ui.exportView.deleteExport": "删除导出的项目",
|
||||||
|
"ui.exportView.deleteExport.desc": "你确定要删除 {{exportName}} 吗?"
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -82,10 +82,9 @@ export default function ReviewCard({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(
|
toast.success(t("ui.dialog.export.toast.success"), {
|
||||||
"Successfully started export. View the file in the /exports folder.",
|
position: "top-center",
|
||||||
{ position: "top-center" },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { DateRange } from "react-day-picker";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type CalendarFilterButtonProps = {
|
type CalendarFilterButtonProps = {
|
||||||
reviewSummary?: ReviewSummary;
|
reviewSummary?: ReviewSummary;
|
||||||
@ -64,7 +65,7 @@ export default function CalendarFilterButton({
|
|||||||
updateSelectedDay(undefined);
|
updateSelectedDay(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -67,6 +67,7 @@ import {
|
|||||||
MobilePageTitle,
|
MobilePageTitle,
|
||||||
} from "../mobile/MobilePage";
|
} from "../mobile/MobilePage";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
type CameraGroupSelectorProps = {
|
type CameraGroupSelectorProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -341,9 +342,11 @@ function NewGroupDialog({
|
|||||||
className={cn(isDesktop && "mt-5", "justify-center")}
|
className={cn(isDesktop && "mt-5", "justify-center")}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<Title>Camera Groups</Title>
|
<Title>
|
||||||
|
<Trans>ui.cameraGroup</Trans>
|
||||||
|
</Title>
|
||||||
<Description className="sr-only">
|
<Description className="sr-only">
|
||||||
Edit camera groups
|
<Trans>ui.cameraGroup.edit</Trans>
|
||||||
</Description>
|
</Description>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -391,7 +394,11 @@ function NewGroupDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Title>
|
<Title>
|
||||||
{editState == "add" ? "Add" : "Edit"} Camera Group
|
{editState == "add" ? (
|
||||||
|
<Trans>ui.cameraGroup.add</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>ui.cameraGroup.edit</Trans>
|
||||||
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
<Description className="sr-only">
|
<Description className="sr-only">
|
||||||
Edit camera groups
|
Edit camera groups
|
||||||
@ -464,8 +471,12 @@ export function EditGroupDialog({
|
|||||||
>
|
>
|
||||||
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
|
<div className="scrollbar-container flex flex-col overflow-y-auto md:my-4">
|
||||||
<Header className="mt-2" onClose={() => setOpen(false)}>
|
<Header className="mt-2" onClose={() => setOpen(false)}>
|
||||||
<Title>Edit Camera Group</Title>
|
<Title>
|
||||||
<Description className="sr-only">Edit camera group</Description>
|
<Trans>ui.cameraGroup.edit</Trans>
|
||||||
|
</Title>
|
||||||
|
<Description className="sr-only">
|
||||||
|
<Trans>ui.cameraGroup.edit.desc</Trans>
|
||||||
|
</Description>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<CameraGroupEdit
|
<CameraGroupEdit
|
||||||
@ -515,19 +526,24 @@ export function CameraGroupRow({
|
|||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
<Trans>ui.cameraGroup.delete.confirm</Trans>
|
||||||
|
</AlertDialogTitle>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Are you sure you want to delete the camera group{" "}
|
<Trans values={{ name: group[0] }}>
|
||||||
<em>{group[0]}</em>?
|
ui.cameraGroup.delete.confirm.desc
|
||||||
|
</Trans>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
|
<Trans>ui.cancel</Trans>
|
||||||
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className={buttonVariants({ variant: "destructive" })}
|
className={buttonVariants({ variant: "destructive" })}
|
||||||
onClick={onDeleteGroup}
|
onClick={onDeleteGroup}
|
||||||
>
|
>
|
||||||
Delete
|
<Trans>ui.delete</Trans>
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
@ -568,7 +584,9 @@ export function CameraGroupRow({
|
|||||||
onClick={onEditGroup}
|
onClick={onEditGroup}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Edit</TooltipContent>
|
<TooltipContent>
|
||||||
|
<Trans>ui.edit</Trans>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -579,7 +597,9 @@ export function CameraGroupRow({
|
|||||||
onClick={() => setDeleteDialogOpen(true)}
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Delete</TooltipContent>
|
<TooltipContent>
|
||||||
|
<Trans>ui.delete</Trans>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -614,7 +634,7 @@ export function CameraGroupEdit({
|
|||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, {
|
.min(2, {
|
||||||
message: "Camera group name must be at least 2 characters.",
|
message: t("ui.cameraGroup.name.errorMessage.mustLeastCharacters"),
|
||||||
})
|
})
|
||||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||||
.refine(
|
.refine(
|
||||||
@ -625,7 +645,7 @@ export function CameraGroupEdit({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Camera group name already exists.",
|
message: t("ui.cameraGroup.name.errorMessage.exists"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@ -633,11 +653,11 @@ export function CameraGroupEdit({
|
|||||||
return !value.includes(".");
|
return !value.includes(".");
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Camera group name must not contain a period.",
|
message: t("ui.cameraGroup.name.errorMessage.nameMustNotPeriod"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine((value: string) => value.toLowerCase() !== "default", {
|
.refine((value: string) => value.toLowerCase() !== "default", {
|
||||||
message: "Invalid camera group name.",
|
message: t("ui.cameraGroup.name.errorMessage.invalid"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cameras: z.array(z.string()),
|
cameras: z.array(z.string()),
|
||||||
@ -682,22 +702,30 @@ export function CameraGroupEdit({
|
|||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(`Camera group (${values.name}) has been saved.`, {
|
toast.success(
|
||||||
|
t("ui.cameraGroup.toast.success", { name: values.name }),
|
||||||
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
updateConfig();
|
updateConfig();
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
onSave();
|
onSave();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Failed to save config changes: ${res.statusText}`, {
|
toast.error(
|
||||||
|
t("ui.cameraGroup.toast.error", { error: res.statusText }),
|
||||||
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to save config changes: ${error.response.data.message}`,
|
t("ui.cameraGroup.toast.error", {
|
||||||
|
error: error.response.data.message,
|
||||||
|
}),
|
||||||
{ position: "top-center" },
|
{ position: "top-center" },
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -729,11 +757,13 @@ export function CameraGroupEdit({
|
|||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>
|
||||||
|
<Trans>ui.cameraGroup.name</Trans>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
|
||||||
placeholder="Enter a name..."
|
placeholder={t("ui.cameraGroup.name.placeholder")}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -749,9 +779,11 @@ export function CameraGroupEdit({
|
|||||||
name="cameras"
|
name="cameras"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cameras</FormLabel>
|
<FormLabel>
|
||||||
|
<Trans>ui.cameraGroup.cameras</Trans>
|
||||||
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Select cameras for this group.
|
<Trans>ui.cameraGroup.cameras.desc</Trans>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{[
|
{[
|
||||||
@ -782,7 +814,9 @@ export function CameraGroupEdit({
|
|||||||
name="icon"
|
name="icon"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col space-y-2">
|
<FormItem className="flex flex-col space-y-2">
|
||||||
<FormLabel>Icon</FormLabel>
|
<FormLabel>
|
||||||
|
<Trans>ui.cameraGroup.icon</Trans>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
selectedIcon={{
|
selectedIcon={{
|
||||||
@ -810,7 +844,7 @@ export function CameraGroupEdit({
|
|||||||
aria-label="Cancel"
|
aria-label="Cancel"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>ui.cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
@ -822,10 +856,12 @@ export function CameraGroupEdit({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
<span>Saving...</span>
|
<span>
|
||||||
|
<Trans>ui.saving</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"Save"
|
<Trans>ui.save</Trans>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
|||||||
import FilterSwitch from "./FilterSwitch";
|
import FilterSwitch from "./FilterSwitch";
|
||||||
import { FaVideo } from "react-icons/fa";
|
import { FaVideo } from "react-icons/fa";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type CameraFilterButtonProps = {
|
type CameraFilterButtonProps = {
|
||||||
allCameras: string[];
|
allCameras: string[];
|
||||||
@ -139,7 +140,7 @@ export function CamerasFilterContent({
|
|||||||
{isMobile && (
|
{isMobile && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="flex justify-center">
|
<DropdownMenuLabel className="flex justify-center">
|
||||||
Cameras
|
<Trans>ui.filter.allCameras.short</Trans>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
@ -147,7 +148,7 @@ export function CamerasFilterContent({
|
|||||||
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
|
<div className="scrollbar-container flex h-auto max-h-[80dvh] flex-col gap-2 overflow-y-auto overflow-x-hidden p-4">
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
isChecked={currentCameras == undefined}
|
isChecked={currentCameras == undefined}
|
||||||
label="All Cameras"
|
label={t("ui.filter.allCameras")}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
setCurrentCameras(undefined);
|
setCurrentCameras(undefined);
|
||||||
@ -212,7 +213,7 @@ export function CamerasFilterContent({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
<Trans>ui.apply</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Reset"
|
aria-label="Reset"
|
||||||
@ -221,7 +222,7 @@ export function CamerasFilterContent({
|
|||||||
updateCameraFilter(undefined);
|
updateCameraFilter(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -354,7 +354,7 @@ function GeneralFilterButton({
|
|||||||
: "text-primary"
|
: "text-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Trans>ui.reviewFilter.filter</Trans>
|
<Trans>ui.filter</Trans>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -462,7 +462,7 @@ export function GeneralFilterContent({
|
|||||||
className="mx-2 cursor-pointer text-primary"
|
className="mx-2 cursor-pointer text-primary"
|
||||||
htmlFor="allLabels"
|
htmlFor="allLabels"
|
||||||
>
|
>
|
||||||
<Trans>ui.reviewFilter.filter.allLabels</Trans>
|
<Trans>ui.filter.allLabels</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
@ -509,7 +509,7 @@ export function GeneralFilterContent({
|
|||||||
className="mx-2 cursor-pointer text-primary"
|
className="mx-2 cursor-pointer text-primary"
|
||||||
htmlFor="allZones"
|
htmlFor="allZones"
|
||||||
>
|
>
|
||||||
<Trans>ui.reviewFilter.filter.allZones</Trans>
|
<Trans>ui.filter.allZones</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
|||||||
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
import SearchFilterDialog from "../overlay/dialog/SearchFilterDialog";
|
||||||
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
import { CalendarRangeFilterButton } from "./CalendarFilterButton";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
type SearchFilterGroupProps = {
|
type SearchFilterGroupProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -190,7 +192,9 @@ export default function SearchFilterGroup({
|
|||||||
to: new Date(filter.before * 1000),
|
to: new Date(filter.before * 1000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultText={isMobile ? "Dates" : "All Dates"}
|
defaultText={
|
||||||
|
isMobile ? t("ui.filter.allDates.short") : t("ui.filter.allDates")
|
||||||
|
}
|
||||||
updateSelectedRange={onUpdateSelectedRange}
|
updateSelectedRange={onUpdateSelectedRange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -231,18 +235,18 @@ function GeneralFilterButton({
|
|||||||
|
|
||||||
const buttonText = useMemo(() => {
|
const buttonText = useMemo(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return "Labels";
|
return t("ui.filter.allLabels.short");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedLabels || selectedLabels.length == 0) {
|
if (!selectedLabels || selectedLabels.length == 0) {
|
||||||
return "All Labels";
|
return t("ui.filter.allLabels");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLabels.length == 1) {
|
if (selectedLabels.length == 1) {
|
||||||
return selectedLabels[0];
|
return t("object." + selectedLabels[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${selectedLabels.length} Labels`;
|
return t("ui.filter.countLabels", { count: selectedLabels.length });
|
||||||
}, [selectedLabels]);
|
}, [selectedLabels]);
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
@ -326,7 +330,7 @@ export function GeneralFilterContent({
|
|||||||
className="mx-2 cursor-pointer text-primary"
|
className="mx-2 cursor-pointer text-primary"
|
||||||
htmlFor="allLabels"
|
htmlFor="allLabels"
|
||||||
>
|
>
|
||||||
All Labels
|
<Trans>ui.filter.allLabels</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
@ -343,7 +347,7 @@ export function GeneralFilterContent({
|
|||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
label={item.replaceAll("_", " ")}
|
label={t("object." + item)}
|
||||||
isChecked={currentLabels?.includes(item) ?? false}
|
isChecked={currentLabels?.includes(item) ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
@ -378,7 +382,7 @@ export function GeneralFilterContent({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
<Trans>ui.apply</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Reset"
|
aria-label="Reset"
|
||||||
@ -387,7 +391,7 @@ export function GeneralFilterContent({
|
|||||||
updateLabelFilter(undefined);
|
updateLabelFilter(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -436,7 +440,7 @@ function SortTypeButton({
|
|||||||
<div
|
<div
|
||||||
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
|
className={`${selectedSortType != defaultSortType && selectedSortType != undefined ? "text-selected-foreground" : "text-primary"}`}
|
||||||
>
|
>
|
||||||
Sort
|
<Trans>ui.filter.sort</Trans>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@ -492,11 +496,11 @@ export function SortTypeContent({
|
|||||||
onClose,
|
onClose,
|
||||||
}: SortTypeContentProps) {
|
}: SortTypeContentProps) {
|
||||||
const sortLabels = {
|
const sortLabels = {
|
||||||
date_asc: "Date (Ascending)",
|
date_asc: t("ui.filter.sort.dateAsc"),
|
||||||
date_desc: "Date (Descending)",
|
date_desc: t("ui.filter.sort.dateDesc"),
|
||||||
score_asc: "Object Score (Ascending)",
|
score_asc: t("ui.filter.sort.scoreAsc"),
|
||||||
score_desc: "Object Score (Descending)",
|
score_desc: t("ui.filter.sort.scoreDesc"),
|
||||||
relevance: "Relevance",
|
relevance: t("ui.filter.sort.relevance"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -551,7 +555,7 @@ export function SortTypeContent({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
<Trans>ui.apply</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Reset"
|
aria-label="Reset"
|
||||||
@ -560,7 +564,7 @@ export function SortTypeContent({
|
|||||||
updateSortType(undefined);
|
updateSortType(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import { IoClose } from "react-icons/io5";
|
|||||||
import Heading from "../ui/heading";
|
import Heading from "../ui/heading";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
export type IconName = keyof typeof LuIcons;
|
export type IconName = keyof typeof LuIcons;
|
||||||
|
|
||||||
@ -70,7 +72,7 @@ export default function IconPicker({
|
|||||||
className="mt-2 w-full text-muted-foreground"
|
className="mt-2 w-full text-muted-foreground"
|
||||||
aria-label="Select an icon"
|
aria-label="Select an icon"
|
||||||
>
|
>
|
||||||
Select an icon
|
<Trans>ui.iconPicker.selectIcon</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<div className="hover:cursor-pointer">
|
<div className="hover:cursor-pointer">
|
||||||
@ -101,7 +103,9 @@ export default function IconPicker({
|
|||||||
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
|
className="flex max-h-[50dvh] flex-col overflow-y-hidden md:max-h-[30dvh]"
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex flex-row items-center justify-between">
|
<div className="mb-3 flex flex-row items-center justify-between">
|
||||||
<Heading as="h4">Select an icon</Heading>
|
<Heading as="h4">
|
||||||
|
<Trans>ui.iconPicker.selectIcon</Trans>
|
||||||
|
</Heading>
|
||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
<IoClose
|
<IoClose
|
||||||
size={15}
|
size={15}
|
||||||
@ -113,7 +117,7 @@ export default function IconPicker({
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for an icon..."
|
placeholder={t("ui.iconPicker.search.placeholder")}
|
||||||
className="text-md mb-3 md:text-sm"
|
className="text-md mb-3 md:text-sm"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import { getUTCOffset } from "@/utils/dateUtil";
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
import { GenericVideoPlayer } from "../player/GenericVideoPlayer";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
const EXPORT_OPTIONS = [
|
const EXPORT_OPTIONS = [
|
||||||
"1",
|
"1",
|
||||||
@ -68,12 +70,14 @@ export default function ExportDialog({
|
|||||||
|
|
||||||
const onStartExport = useCallback(() => {
|
const onStartExport = useCallback(() => {
|
||||||
if (!range) {
|
if (!range) {
|
||||||
toast.error("No valid time range selected", { position: "top-center" });
|
toast.error(t("ui.dialog.export.toast.error.noVaildTimeSelected"), {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.before < range.after) {
|
if (range.before < range.after) {
|
||||||
toast.error("End time must be after start time", {
|
toast.error(t("ui.dialog.export.toast.error.endTimeMustAfterStartTime"), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -89,10 +93,9 @@ export default function ExportDialog({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(
|
toast.success(t("ui.dialog.export.toast.success"), {
|
||||||
"Successfully started export. View the file in the /exports folder.",
|
position: "top-center",
|
||||||
{ position: "top-center" },
|
});
|
||||||
);
|
|
||||||
setName("");
|
setName("");
|
||||||
setRange(undefined);
|
setRange(undefined);
|
||||||
setMode("none");
|
setMode("none");
|
||||||
@ -100,14 +103,18 @@ export default function ExportDialog({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.response?.data?.message) {
|
if (error.response?.data?.message) {
|
||||||
|
// api error message need to be translated
|
||||||
toast.error(
|
toast.error(
|
||||||
`Failed to start export: ${error.response.data.message}`,
|
`${t("ui.dialog.export.toast.error.failed", { error: error.response.data.message })}`,
|
||||||
{ position: "top-center" },
|
{ position: "top-center" },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.error(`Failed to start export: ${error.message}`, {
|
toast.error(
|
||||||
|
`${t("ui.dialog.export.toast.error.failed", { error: error.message })}`,
|
||||||
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [camera, name, range, setRange, setName, setMode]);
|
}, [camera, name, range, setRange, setName, setMode]);
|
||||||
@ -157,7 +164,11 @@ export default function ExportDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
<FaArrowDown className="rounded-md bg-secondary-foreground fill-secondary p-1" />
|
||||||
{isDesktop && <div className="text-primary">Export</div>}
|
{isDesktop && (
|
||||||
|
<div className="text-primary">
|
||||||
|
<Trans>ui.menu.export</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<Content
|
<Content
|
||||||
@ -250,7 +261,9 @@ export function ExportContent({
|
|||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Export</DialogTitle>
|
<DialogTitle>
|
||||||
|
<Trans>ui.menu.export</Trans>
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<SelectSeparator className="my-4 bg-secondary" />
|
<SelectSeparator className="my-4 bg-secondary" />
|
||||||
</>
|
</>
|
||||||
@ -274,9 +287,11 @@ export function ExportContent({
|
|||||||
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
<Label className="cursor-pointer capitalize" htmlFor={opt}>
|
||||||
{isNaN(parseInt(opt))
|
{isNaN(parseInt(opt))
|
||||||
? opt == "timeline"
|
? opt == "timeline"
|
||||||
? "Select from Timeline"
|
? t("ui.dialog.export.time.fromTimeline")
|
||||||
: `${opt}`
|
: t("ui.dialog.export.time." + opt)
|
||||||
: `Last ${opt > "1" ? `${opt} Hours` : "Hour"}`}
|
: t("ui.dialog.export.time.lastHour", {
|
||||||
|
count: parseInt(opt),
|
||||||
|
})}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -292,7 +307,7 @@ export function ExportContent({
|
|||||||
<Input
|
<Input
|
||||||
className="text-md my-6"
|
className="text-md my-6"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Name the Export"
|
placeholder={t("ui.dialog.export.name.placeholder")}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -304,7 +319,7 @@ export function ExportContent({
|
|||||||
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
|
className={`cursor-pointer p-2 text-center ${isDesktop ? "" : "w-full"}`}
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>ui.cancel</Trans>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className={isDesktop ? "" : "w-full"}
|
className={isDesktop ? "" : "w-full"}
|
||||||
@ -322,7 +337,9 @@ export function ExportContent({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedOption == "timeline" ? "Select" : "Export"}
|
{selectedOption == "timeline"
|
||||||
|
? t("ui.dialog.export.select")
|
||||||
|
: t("ui.dialog.export.export")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
@ -382,14 +399,14 @@ function CustomTimeSelector({
|
|||||||
const formattedStart = useFormattedTimestamp(
|
const formattedStart = useFormattedTimestamp(
|
||||||
startTime,
|
startTime,
|
||||||
config?.ui.time_format == "24hour"
|
config?.ui.time_format == "24hour"
|
||||||
? "%b %-d, %H:%M:%S"
|
? t("ui.time.formattedTimestamp.24hour")
|
||||||
: "%b %-d, %I:%M:%S %p",
|
: t("ui.time.formattedTimestamp"),
|
||||||
);
|
);
|
||||||
const formattedEnd = useFormattedTimestamp(
|
const formattedEnd = useFormattedTimestamp(
|
||||||
endTime,
|
endTime,
|
||||||
config?.ui.time_format == "24hour"
|
config?.ui.time_format == "24hour"
|
||||||
? "%b %-d, %H:%M:%S"
|
? t("ui.time.formattedTimestamp.24hour")
|
||||||
: "%b %-d, %I:%M:%S %p",
|
: t("ui.time.formattedTimestamp"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const startClock = useMemo(() => {
|
const startClock = useMemo(() => {
|
||||||
@ -576,9 +593,11 @@ export function ExportPreviewDialog({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Preview Export</DialogTitle>
|
<DialogTitle>
|
||||||
|
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
Preview Export
|
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<GenericVideoPlayer source={source} />
|
<GenericVideoPlayer source={source} />
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { toast } from "sonner";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import SaveExportOverlay from "./SaveExportOverlay";
|
import SaveExportOverlay from "./SaveExportOverlay";
|
||||||
import { isIOS, isMobile } from "react-device-detect";
|
import { isIOS, isMobile } from "react-device-detect";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
|
||||||
|
|
||||||
@ -89,10 +90,9 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(
|
toast.success(t("ui.dialog.export.toast.success"), {
|
||||||
"Successfully started export. View the file in the /exports folder.",
|
position: "top-center",
|
||||||
{ position: "top-center" },
|
});
|
||||||
);
|
|
||||||
setName("");
|
setName("");
|
||||||
setRange(undefined);
|
setRange(undefined);
|
||||||
setMode("none");
|
setMode("none");
|
||||||
@ -238,7 +238,7 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { LuVideo, LuX } from "react-icons/lu";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { FaCompactDisc } from "react-icons/fa";
|
import { FaCompactDisc } from "react-icons/fa";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type SaveExportOverlayProps = {
|
type SaveExportOverlayProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -33,7 +34,7 @@ export default function SaveExportOverlay({
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
<LuX />
|
<LuX />
|
||||||
Cancel
|
<Trans>ui.cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
@ -42,7 +43,7 @@ export default function SaveExportOverlay({
|
|||||||
onClick={onPreview}
|
onClick={onPreview}
|
||||||
>
|
>
|
||||||
<LuVideo />
|
<LuVideo />
|
||||||
Preview Export
|
<Trans>ui.dialog.export.fromTimeline.previewExport</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
@ -52,7 +53,7 @@ export default function SaveExportOverlay({
|
|||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
>
|
>
|
||||||
<FaCompactDisc />
|
<FaCompactDisc />
|
||||||
Save Export
|
<Trans>ui.dialog.export.fromTimeline.saveExport</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type SearchFilterDialogProps = {
|
type SearchFilterDialogProps = {
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
@ -91,7 +92,7 @@ export default function SearchFilterDialog({
|
|||||||
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
moreFiltersSelected ? "text-white" : "text-secondary-foreground",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
More Filters
|
<Trans>ui.filter.more</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
const content = (
|
const content = (
|
||||||
@ -165,7 +166,7 @@ export default function SearchFilterDialog({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
<Trans>ui.apply</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Reset filters to default values"
|
aria-label="Reset filters to default values"
|
||||||
@ -183,7 +184,7 @@ export default function SearchFilterDialog({
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,7 +261,9 @@ function TimeRangeFilterContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<div className="text-lg">Time Range</div>
|
<div className="text-lg">
|
||||||
|
<Trans>ui.filter.timeRange</Trans>
|
||||||
|
</div>
|
||||||
<div className="mt-3 flex flex-row items-center justify-center gap-2">
|
<div className="mt-3 flex flex-row items-center justify-center gap-2">
|
||||||
<Popover
|
<Popover
|
||||||
open={startOpen}
|
open={startOpen}
|
||||||
@ -358,7 +361,9 @@ export function ZoneFilterContent({
|
|||||||
<>
|
<>
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<DropdownMenuSeparator className="mb-3" />
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
<div className="text-lg">Zones</div>
|
<div className="text-lg">
|
||||||
|
<Trans>ui.filter.zones</Trans>
|
||||||
|
</div>
|
||||||
{allZones && (
|
{allZones && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||||
@ -366,7 +371,7 @@ export function ZoneFilterContent({
|
|||||||
className="mx-2 cursor-pointer text-primary"
|
className="mx-2 cursor-pointer text-primary"
|
||||||
htmlFor="allZones"
|
htmlFor="allZones"
|
||||||
>
|
>
|
||||||
All Zones
|
<Trans>ui.filter.allZones</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
@ -424,10 +429,12 @@ export function SubFilterContent({
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<DropdownMenuSeparator className="mb-3" />
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
<div className="text-lg">Sub Labels</div>
|
<div className="text-lg">
|
||||||
|
<Trans>ui.filter.subLabels</Trans>
|
||||||
|
</div>
|
||||||
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
<div className="mb-5 mt-2.5 flex items-center justify-between">
|
||||||
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
|
<Label className="mx-2 cursor-pointer text-primary" htmlFor="allLabels">
|
||||||
All Sub Labels
|
<Trans>ui.filter.allSubLabels</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
@ -482,7 +489,9 @@ export function ScoreFilterContent({
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<DropdownMenuSeparator className="mb-3" />
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
<div className="mb-3 text-lg">Score</div>
|
<div className="mb-3 text-lg">
|
||||||
|
<Trans>ui.filter.score</Trans>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Input
|
<Input
|
||||||
className="w-14 text-center"
|
className="w-14 text-center"
|
||||||
@ -568,7 +577,9 @@ export function SnapshotClipFilterContent({
|
|||||||
return (
|
return (
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<DropdownMenuSeparator className="mb-3" />
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
<div className="mb-3 text-lg">Features</div>
|
<div className="mb-3 text-lg">
|
||||||
|
<Trans>ui.filter.features</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="my-2.5 space-y-1">
|
<div className="my-2.5 space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -590,7 +601,7 @@ export function SnapshotClipFilterContent({
|
|||||||
htmlFor="snapshot-filter"
|
htmlFor="snapshot-filter"
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
>
|
>
|
||||||
Has a snapshot
|
<Trans>ui.filter.features.hasSnapshot</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
@ -611,14 +622,14 @@ export function SnapshotClipFilterContent({
|
|||||||
aria-label="Yes"
|
aria-label="Yes"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
Yes
|
<Trans>ui.yes</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="no"
|
value="no"
|
||||||
aria-label="No"
|
aria-label="No"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
No
|
<Trans>ui.no</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
@ -652,12 +663,9 @@ export function SnapshotClipFilterContent({
|
|||||||
side="left"
|
side="left"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
>
|
>
|
||||||
You must first filter on tracked objects that have a
|
<Trans>
|
||||||
snapshot.
|
ui.filter.features.submittedToFrigatePlus.tips
|
||||||
<br />
|
</Trans>
|
||||||
<br />
|
|
||||||
Tracked objects without a snapshot cannot be submitted to
|
|
||||||
Frigate+.
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -666,7 +674,7 @@ export function SnapshotClipFilterContent({
|
|||||||
htmlFor="plus-filter"
|
htmlFor="plus-filter"
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
>
|
>
|
||||||
Submitted to Frigate+
|
<Trans>ui.filter.features.submittedToFrigatePlus</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
@ -692,14 +700,14 @@ export function SnapshotClipFilterContent({
|
|||||||
aria-label="Yes"
|
aria-label="Yes"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
Yes
|
<Trans>ui.yes</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="no"
|
value="no"
|
||||||
aria-label="No"
|
aria-label="No"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
No
|
<Trans>ui.no</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
@ -728,7 +736,7 @@ export function SnapshotClipFilterContent({
|
|||||||
htmlFor="clip-filter"
|
htmlFor="clip-filter"
|
||||||
className="cursor-pointer text-sm font-medium leading-none"
|
className="cursor-pointer text-sm font-medium leading-none"
|
||||||
>
|
>
|
||||||
Has a video clip
|
<Trans>ui.filter.features.hasVideoClip</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
@ -747,14 +755,14 @@ export function SnapshotClipFilterContent({
|
|||||||
aria-label="Yes"
|
aria-label="Yes"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
Yes
|
<Trans>ui.yes</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value="no"
|
value="no"
|
||||||
aria-label="No"
|
aria-label="No"
|
||||||
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
className="data-[state=on]:bg-selected data-[state=on]:text-white data-[state=on]:hover:bg-selected data-[state=on]:hover:text-white"
|
||||||
>
|
>
|
||||||
No
|
<Trans>ui.no</Trans>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
getPreviewForTimeRange,
|
getPreviewForTimeRange,
|
||||||
usePreviewForTimeRange,
|
usePreviewForTimeRange,
|
||||||
} from "@/hooks/use-camera-previews";
|
} from "@/hooks/use-camera-previews";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -88,7 +89,7 @@ export default function PreviewPlayer({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
No Preview Found
|
<Trans>ui.player.noPreviewFound</Trans>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -324,7 +325,9 @@ function PreviewVideoPlayer({
|
|||||||
</video>
|
</video>
|
||||||
{cameraPreviews && !currentPreview && (
|
{cameraPreviews && !currentPreview && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
||||||
No Preview Found for {camera.replaceAll("_", " ")}
|
<Trans value={{ camera: camera.replaceAll("_", " ") }}>
|
||||||
|
ui.player.noPreviewFoundFor
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||||
@ -544,7 +547,9 @@ function PreviewFramesPlayer({
|
|||||||
/>
|
/>
|
||||||
{previewFrames?.length === 0 && (
|
{previewFrames?.length === 0 && (
|
||||||
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
||||||
No Preview Found for {camera.replaceAll("_", " ")}
|
<Trans values={{ cameraName: camera.replaceAll("_", " ") }}>
|
||||||
|
ui.player.noPreviewFoundFor
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
{firstLoad && <Skeleton className="absolute aspect-video size-full" />}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically switches between video playback and scrubbing preview player.
|
* Dynamically switches between video playback and scrubbing preview player.
|
||||||
@ -247,7 +248,7 @@ export default function DynamicVideoPlayer({
|
|||||||
)}
|
)}
|
||||||
{!isScrubbing && !isLoading && noRecording && (
|
{!isScrubbing && !isLoading && noRecording && (
|
||||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
No recordings found for this time
|
<Trans>ui.player.noRecordingsFoundForThisTime</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -204,7 +204,9 @@ export default function MotionMaskEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Edit Motion Mask - Frigate";
|
document.title = t(
|
||||||
|
"ui.settingView.masksAndZonesSettings.motionMasks.documentTitle",
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
|
|||||||
@ -238,7 +238,9 @@ export default function ObjectMaskEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Edit Object Mask - Frigate";
|
document.title = t(
|
||||||
|
"ui.settingView.masksAndZonesSettings.objectMasks.documentTitle",
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import FilterSwitch from "../filter/FilterSwitch";
|
|||||||
import { SearchFilter, SearchSource } from "@/types/search";
|
import { SearchFilter, SearchSource } from "@/types/search";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
type SearchSettingsProps = {
|
type ExploreSettingsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
columns: number;
|
columns: number;
|
||||||
defaultView: string;
|
defaultView: string;
|
||||||
@ -27,7 +29,7 @@ type SearchSettingsProps = {
|
|||||||
setDefaultView: (view: string) => void;
|
setDefaultView: (view: string) => void;
|
||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
};
|
};
|
||||||
export default function SearchSettings({
|
export default function ExploreSettings({
|
||||||
className,
|
className,
|
||||||
columns,
|
columns,
|
||||||
setColumns,
|
setColumns,
|
||||||
@ -35,7 +37,7 @@ export default function SearchSettings({
|
|||||||
filter,
|
filter,
|
||||||
setDefaultView,
|
setDefaultView,
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
}: SearchSettingsProps) {
|
}: ExploreSettingsProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@ -46,21 +48,22 @@ export default function SearchSettings({
|
|||||||
const trigger = (
|
const trigger = (
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
aria-label="Search Settings"
|
aria-label="Explore Settings"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaCog className="text-secondary-foreground" />
|
<FaCog className="text-secondary-foreground" />
|
||||||
Settings
|
<Trans>ui.searchView.settings</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
const content = (
|
const content = (
|
||||||
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
|
<div className={cn(className, "my-3 space-y-5 py-3 md:mt-0 md:py-0")}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="text-md">Default View</div>
|
<div className="text-md">
|
||||||
|
<Trans>ui.searchView.settings.defaultView</Trans>
|
||||||
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
When no filters are selected, display a summary of the most recent
|
<Trans>ui.searchView.settings.defaultView.desc</Trans>
|
||||||
tracked objects per label, or display an unfiltered grid.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@ -68,7 +71,9 @@ export default function SearchSettings({
|
|||||||
onValueChange={(value) => setDefaultView(value)}
|
onValueChange={(value) => setDefaultView(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
{defaultView == "summary" ? "Summary" : "Unfiltered Grid"}
|
{defaultView == "summary"
|
||||||
|
? t("ui.searchView.settings.defaultView.summary")
|
||||||
|
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@ -78,7 +83,9 @@ export default function SearchSettings({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
value={value}
|
value={value}
|
||||||
>
|
>
|
||||||
{value == "summary" ? "Summary" : "Unfiltered Grid"}
|
{value == "summary"
|
||||||
|
? t("ui.searchView.settings.defaultView.summary")
|
||||||
|
: t("ui.searchView.settings.defaultView.unfilteredGrid")}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@ -90,9 +97,11 @@ export default function SearchSettings({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex w-full flex-col space-y-4">
|
<div className="flex w-full flex-col space-y-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="text-md">Grid Columns</div>
|
<div className="text-md">
|
||||||
|
<Trans>ui.searchView.settings.gridColumns</Trans>
|
||||||
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
Select the number of columns in the grid view.
|
<Trans>ui.searchView.settings.gridColumns.desc</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
@ -153,10 +162,11 @@ export function SearchTypeContent({
|
|||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
<DropdownMenuSeparator className="mb-3" />
|
<DropdownMenuSeparator className="mb-3" />
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="text-md">Search Source</div>
|
<div className="text-md">
|
||||||
|
<Trans>ui.searchView.settings.searchSource</Trans>
|
||||||
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
Choose whether to search the thumbnails or descriptions of your
|
<Trans>ui.searchView.settings.searchSource.desc</Trans>
|
||||||
tracked objects.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2.5 flex flex-col gap-2.5">
|
<div className="mt-2.5 flex flex-col gap-2.5">
|
||||||
|
|||||||
@ -321,7 +321,9 @@ export default function ZoneEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Edit Zone - Frigate";
|
document.title = t(
|
||||||
|
"ui.settingView.masksAndZonesSettings.zone.documentTitle",
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import {
|
|||||||
import { Switch } from "./switch";
|
import { Switch } from "./switch";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LuCheck } from "react-icons/lu";
|
import { LuCheck } from "react-icons/lu";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
export interface DateRangePickerProps {
|
export interface DateRangePickerProps {
|
||||||
/** Click handler for applying the updates from DateRangePicker. */
|
/** Click handler for applying the updates from DateRangePicker. */
|
||||||
@ -59,15 +61,15 @@ interface Preset {
|
|||||||
|
|
||||||
// Define presets
|
// Define presets
|
||||||
const PRESETS: Preset[] = [
|
const PRESETS: Preset[] = [
|
||||||
{ name: "today", label: "Today" },
|
{ name: "today", label: t("ui.time.today") },
|
||||||
{ name: "yesterday", label: "Yesterday" },
|
{ name: "yesterday", label: t("ui.time.yesterday") },
|
||||||
{ name: "last7", label: "Last 7 days" },
|
{ name: "last7", label: t("ui.time.last7") },
|
||||||
{ name: "last14", label: "Last 14 days" },
|
{ name: "last14", label: t("ui.time.last14") },
|
||||||
{ name: "last30", label: "Last 30 days" },
|
{ name: "last30", label: t("ui.time.last30") },
|
||||||
{ name: "thisWeek", label: "This Week" },
|
{ name: "thisWeek", label: t("ui.time.thisWeek") },
|
||||||
{ name: "lastWeek", label: "Last Week" },
|
{ name: "lastWeek", label: t("ui.time.lastWeek") },
|
||||||
{ name: "thisMonth", label: "This Month" },
|
{ name: "thisMonth", label: t("ui.time.thisMonth") },
|
||||||
{ name: "lastMonth", label: "Last Month" },
|
{ name: "lastMonth", label: t("ui.time.lastMonth") },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** The DateRangePicker component allows a user to select a range of dates */
|
/** The DateRangePicker component allows a user to select a range of dates */
|
||||||
@ -429,7 +431,7 @@ export function DateRangePicker({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
<Trans>ui.apply</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -440,7 +442,7 @@ export function DateRangePicker({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label="Reset"
|
aria-label="Reset"
|
||||||
>
|
>
|
||||||
Reset
|
<Trans>ui.reset</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,24 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { DayPicker } from "react-day-picker";
|
import { DayPicker } from "react-day-picker";
|
||||||
|
import { enUS, Locale, zhCN } from "date-fns/locale";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import i18n from "@/utils/i18n";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
|
||||||
|
let locale: Locale;
|
||||||
|
switch(i18n.language) {
|
||||||
|
case "zh-CN":
|
||||||
|
locale = zhCN;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
locale = enUS;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
classNames,
|
classNames,
|
||||||
@ -15,6 +27,7 @@ function Calendar({
|
|||||||
}: CalendarProps) {
|
}: CalendarProps) {
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
|
locale={locale}
|
||||||
showOutsideDays={showOutsideDays}
|
showOutsideDays={showOutsideDays}
|
||||||
className={cn("p-3", className)}
|
className={cn("p-3", className)}
|
||||||
classNames={{
|
classNames={{
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
import EventView from "@/views/events/EventView";
|
import EventView from "@/views/events/EventView";
|
||||||
import { RecordingView } from "@/views/recording/RecordingView";
|
import { RecordingView } from "@/views/recording/RecordingView";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { t } from "i18next";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -76,9 +77,9 @@ export default function Events() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recording) {
|
if (recording) {
|
||||||
document.title = "Recordings - Frigate";
|
document.title = t("ui.review.recordings.documentTitle");
|
||||||
} else {
|
} else {
|
||||||
document.title = `Review - Frigate`;
|
document.title = t("ui.review.documentTitle");
|
||||||
}
|
}
|
||||||
}, [recording, severity]);
|
}, [recording, severity]);
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,10 @@ import { useSearchEffect } from "@/hooks/use-overlay-state";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DeleteClipType, Export } from "@/types/export";
|
import { DeleteClipType, Export } from "@/types/export";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { t } from "i18next";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
import { LuFolderX } from "react-icons/lu";
|
import { LuFolderX } from "react-icons/lu";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -27,7 +29,7 @@ function Exports() {
|
|||||||
const { data: exports, mutate } = useSWR<Export[]>("exports");
|
const { data: exports, mutate } = useSWR<Export[]>("exports");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Export - Frigate";
|
document.title = t("ui.exportView.documentTitle");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
@ -116,20 +118,26 @@ function Exports() {
|
|||||||
>
|
>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete Export</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
|
<Trans>ui.exportView.deleteExport</Trans>
|
||||||
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Are you sure you want to delete {deleteClip?.exportName}?
|
<Trans values={{ exportName: deleteClip?.exportName }}>
|
||||||
|
ui.exportView.deleteExport.desc
|
||||||
|
</Trans>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
|
<Trans>ui.cancel</Trans>
|
||||||
|
</AlertDialogCancel>
|
||||||
<Button
|
<Button
|
||||||
className="text-white"
|
className="text-white"
|
||||||
aria-label="Delete Export"
|
aria-label="Delete Export"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => onHandleDelete()}
|
onClick={() => onHandleDelete()}
|
||||||
>
|
>
|
||||||
Delete
|
<Trans>ui.delete</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
@ -177,7 +185,7 @@ function Exports() {
|
|||||||
<div className="flex w-full items-center justify-center p-2">
|
<div className="flex w-full items-center justify-center p-2">
|
||||||
<Input
|
<Input
|
||||||
className="text-md w-full bg-muted md:w-1/3"
|
className="text-md w-full bg-muted md:w-1/3"
|
||||||
placeholder="Search"
|
placeholder={t("ui.exportView.search")}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -205,7 +213,7 @@ function Exports() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||||
<LuFolderX className="size-16" />
|
<LuFolderX className="size-16" />
|
||||||
No exports found
|
<Trans>ui.exportView.noExports</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
|||||||
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
import LiveBirdseyeView from "@/views/live/LiveBirdseyeView";
|
||||||
import LiveCameraView from "@/views/live/LiveCameraView";
|
import LiveCameraView from "@/views/live/LiveCameraView";
|
||||||
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
import LiveDashboardView from "@/views/live/LiveDashboardView";
|
||||||
|
import { t } from "i18next";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -64,11 +65,15 @@ function Live() {
|
|||||||
.split("_")
|
.split("_")
|
||||||
.filter((text) => text)
|
.filter((text) => text)
|
||||||
.map((text) => text[0].toUpperCase() + text.substring(1));
|
.map((text) => text[0].toUpperCase() + text.substring(1));
|
||||||
document.title = `${capitalized.join(" ")} - Live - Frigate`;
|
document.title = t("ui.live.documentTitle.withCamera", {
|
||||||
|
camera: capitalized.join(" "),
|
||||||
|
});
|
||||||
} else if (cameraGroup && cameraGroup != "default") {
|
} else if (cameraGroup && cameraGroup != "default") {
|
||||||
document.title = `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)} - Live - Frigate`;
|
document.title = t("ui.live.documentTitle.withCamera", {
|
||||||
|
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
document.title = "Live - Frigate";
|
document.title = t("ui.live.documentTitle");
|
||||||
}
|
}
|
||||||
}, [cameraGroup, selectedCameraName]);
|
}, [cameraGroup, selectedCameraName]);
|
||||||
|
|
||||||
|
|||||||
@ -35,13 +35,13 @@ import MotionTunerView from "@/views/settings/MotionTunerView";
|
|||||||
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
|
||||||
import AuthenticationView from "@/views/settings/AuthenticationView";
|
import AuthenticationView from "@/views/settings/AuthenticationView";
|
||||||
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
import NotificationView from "@/views/settings/NotificationsSettingsView";
|
||||||
import SearchSettingsView from "@/views/settings/SearchSettingsView";
|
import ExploreSettingsView from "@/views/settings/SearchSettingsView";
|
||||||
import UiSettingsView from "@/views/settings/UiSettingsView";
|
import UiSettingsView from "@/views/settings/UiSettingsView";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
const allSettingsViews = [
|
const allSettingsViews = [
|
||||||
"uiSettings",
|
"uiSettings",
|
||||||
"searchSettings",
|
"exploreSettings",
|
||||||
"cameraSettings",
|
"cameraSettings",
|
||||||
"masksAndZones",
|
"masksAndZones",
|
||||||
"motionTuner",
|
"motionTuner",
|
||||||
@ -178,8 +178,8 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
|
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
|
||||||
{page == "uiSettings" && <UiSettingsView />}
|
{page == "uiSettings" && <UiSettingsView />}
|
||||||
{page == "searchSettings" && (
|
{page == "exploreSettings" && (
|
||||||
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
|
<ExploreSettingsView setUnsavedChanges={setUnsavedChanges} />
|
||||||
)}
|
)}
|
||||||
{page == "debug" && (
|
{page == "debug" && (
|
||||||
<ObjectSettingsView selectedCamera={selectedCamera} />
|
<ObjectSettingsView selectedCamera={selectedCamera} />
|
||||||
|
|||||||
@ -32,16 +32,18 @@ i18n
|
|||||||
},
|
},
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
parseMissingKeyHandler: (key: string) => {
|
parseMissingKeyHandler: (key: string) => {
|
||||||
console.debug("Missing key: " + key);
|
const parts = key.split(".");
|
||||||
const parts = key.split('.');
|
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
if (parts[0] === 'object' || parts[0] === 'audio') {
|
if (parts[0] === "object" || parts[0] === "audio") {
|
||||||
return parts[1].replaceAll("_", " ").charAt(0).toUpperCase() + parts[1].slice(1);
|
return (
|
||||||
|
parts[1].replaceAll("_", " ").charAt(0).toUpperCase() +
|
||||||
|
parts[1].slice(1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1];
|
||||||
}
|
}
|
||||||
return key;
|
return key;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@ -54,6 +54,7 @@ import { GiSoundWaves } from "react-icons/gi";
|
|||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
|
import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
type EventViewProps = {
|
type EventViewProps = {
|
||||||
reviewItems?: SegmentedReviewData;
|
reviewItems?: SegmentedReviewData;
|
||||||
@ -194,10 +195,9 @@ export default function EventView({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(
|
toast.success(t("ui.dialog.export.toast.success"), {
|
||||||
"Successfully started export. View the file in the /exports folder.",
|
position: "top-center",
|
||||||
{ position: "top-center" },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@ -412,7 +412,9 @@ export default function LiveCameraView({
|
|||||||
>
|
>
|
||||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div className="text-secondary-foreground">Back</div>
|
<div className="text-secondary-foreground">
|
||||||
|
<Trans>ui.back</Trans>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
|||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useFullscreen } from "@/hooks/use-fullscreen";
|
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
const SEGMENT_DURATION = 30;
|
const SEGMENT_DURATION = 30;
|
||||||
|
|
||||||
@ -385,7 +386,11 @@ export function RecordingView({
|
|||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
>
|
>
|
||||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||||
{isDesktop && <div className="text-primary">Back</div>}
|
{isDesktop && (
|
||||||
|
<div className="text-primary">
|
||||||
|
<Trans>ui.back</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2.5 rounded-lg"
|
className="flex items-center gap-2.5 rounded-lg"
|
||||||
@ -396,7 +401,11 @@ export function RecordingView({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaVideo className="size-5 text-secondary-foreground" />
|
<FaVideo className="size-5 text-secondary-foreground" />
|
||||||
{isDesktop && <div className="text-primary">Live</div>}
|
{isDesktop && (
|
||||||
|
<div className="text-primary">
|
||||||
|
<Trans>ui.menu.live</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
@ -455,14 +464,18 @@ export function RecordingView({
|
|||||||
value="timeline"
|
value="timeline"
|
||||||
aria-label="Select timeline"
|
aria-label="Select timeline"
|
||||||
>
|
>
|
||||||
<div className="">Timeline</div>
|
<div className="">
|
||||||
|
<Trans>ui.review.timeline</Trans>
|
||||||
|
</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
|
className={`${timelineType == "events" ? "" : "text-muted-foreground"}`}
|
||||||
value="events"
|
value="events"
|
||||||
aria-label="Select events"
|
aria-label="Select events"
|
||||||
>
|
>
|
||||||
<div className="">Events</div>
|
<div className="">
|
||||||
|
<Trans>ui.review.events</Trans>
|
||||||
|
</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
) : (
|
) : (
|
||||||
@ -728,7 +741,7 @@ function Timeline({
|
|||||||
>
|
>
|
||||||
{mainCameraReviewItems.length === 0 ? (
|
{mainCameraReviewItems.length === 0 ? (
|
||||||
<div className="mt-5 text-center text-primary">
|
<div className="mt-5 text-center text-primary">
|
||||||
No events found for this time period.
|
<Trans>ui.review.events.noFoundForTimePeriod</Trans>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
mainCameraReviewItems.map((review) => {
|
mainCameraReviewItems.map((review) => {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
||||||
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
||||||
import SearchSettings from "@/components/settings/SearchSettings";
|
import ExploreSettings from "@/components/settings/SearchSettings";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -31,6 +31,7 @@ import {
|
|||||||
import Chip from "@/components/indicators/Chip";
|
import Chip from "@/components/indicators/Chip";
|
||||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||||
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
import SearchActionGroup from "@/components/filter/SearchActionGroup";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
type SearchViewProps = {
|
type SearchViewProps = {
|
||||||
search: string;
|
search: string;
|
||||||
@ -481,7 +482,7 @@ export default function SearchView({
|
|||||||
filter={searchFilter}
|
filter={searchFilter}
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
<SearchSettings
|
<ExploreSettings
|
||||||
columns={columns}
|
columns={columns}
|
||||||
setColumns={setColumns}
|
setColumns={setColumns}
|
||||||
defaultView={defaultView}
|
defaultView={defaultView}
|
||||||
@ -517,7 +518,7 @@ export default function SearchView({
|
|||||||
{uniqueResults?.length == 0 && !isLoading && (
|
{uniqueResults?.length == 0 && !isLoading && (
|
||||||
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
||||||
<LuSearchX className="size-16" />
|
<LuSearchX className="size-16" />
|
||||||
No Tracked Objects Found
|
<Trans>ui.searchView.noTrackedObjects</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -475,8 +475,8 @@ export default function CameraSettingsView({
|
|||||||
|
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{watchedDetectionsZones &&
|
{watchedDetectionsZones &&
|
||||||
watchedDetectionsZones.length > 0
|
watchedDetectionsZones.length > 0 ? (
|
||||||
? !selectDetections ? (
|
!selectDetections ? (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips"
|
i18nKey="ui.settingView.cameraSettings.reviewClassification.zoneObjectDetectionsTips"
|
||||||
values={{
|
values={{
|
||||||
@ -513,7 +513,7 @@ export default function CameraSettingsView({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
) : (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips"
|
i18nKey="ui.settingView.cameraSettings.reviewClassification.objectDetectionsTips"
|
||||||
values={{
|
values={{
|
||||||
|
|||||||
@ -23,19 +23,19 @@ import {
|
|||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
type SearchSettingsViewProps = {
|
type ExploreSettingsViewProps = {
|
||||||
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SearchSettings = {
|
type ExploreSettings = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
reindex?: boolean;
|
reindex?: boolean;
|
||||||
model_size?: SearchModelSize;
|
model_size?: SearchModelSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchSettingsView({
|
export default function ExploreSettingsView({
|
||||||
setUnsavedChanges,
|
setUnsavedChanges,
|
||||||
}: SearchSettingsViewProps) {
|
}: ExploreSettingsViewProps) {
|
||||||
const { data: config, mutate: updateConfig } =
|
const { data: config, mutate: updateConfig } =
|
||||||
useSWR<FrigateConfig>("config");
|
useSWR<FrigateConfig>("config");
|
||||||
const [changedValue, setChangedValue] = useState(false);
|
const [changedValue, setChangedValue] = useState(false);
|
||||||
@ -43,13 +43,14 @@ export default function SearchSettingsView({
|
|||||||
|
|
||||||
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
|
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
|
||||||
|
|
||||||
const [searchSettings, setSearchSettings] = useState<SearchSettings>({
|
const [ExploreSettings, setExploreSettings] = useState<ExploreSettings>({
|
||||||
enabled: undefined,
|
enabled: undefined,
|
||||||
reindex: undefined,
|
reindex: undefined,
|
||||||
model_size: undefined,
|
model_size: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [origSearchSettings, setOrigSearchSettings] = useState<SearchSettings>({
|
const [origExploreSettings, setOrigExploreSettings] =
|
||||||
|
useState<ExploreSettings>({
|
||||||
enabled: undefined,
|
enabled: undefined,
|
||||||
reindex: undefined,
|
reindex: undefined,
|
||||||
model_size: undefined,
|
model_size: undefined,
|
||||||
@ -57,15 +58,15 @@ export default function SearchSettingsView({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
if (config) {
|
||||||
if (searchSettings?.enabled == undefined) {
|
if (ExploreSettings?.enabled == undefined) {
|
||||||
setSearchSettings({
|
setExploreSettings({
|
||||||
enabled: config.semantic_search.enabled,
|
enabled: config.semantic_search.enabled,
|
||||||
reindex: config.semantic_search.reindex,
|
reindex: config.semantic_search.reindex,
|
||||||
model_size: config.semantic_search.model_size,
|
model_size: config.semantic_search.model_size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setOrigSearchSettings({
|
setOrigExploreSettings({
|
||||||
enabled: config.semantic_search.enabled,
|
enabled: config.semantic_search.enabled,
|
||||||
reindex: config.semantic_search.reindex,
|
reindex: config.semantic_search.reindex,
|
||||||
model_size: config.semantic_search.model_size,
|
model_size: config.semantic_search.model_size,
|
||||||
@ -75,8 +76,8 @@ export default function SearchSettingsView({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const handleSearchConfigChange = (newConfig: Partial<SearchSettings>) => {
|
const handleSearchConfigChange = (newConfig: Partial<ExploreSettings>) => {
|
||||||
setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
|
setExploreSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
|
||||||
setUnsavedChanges(true);
|
setUnsavedChanges(true);
|
||||||
setChangedValue(true);
|
setChangedValue(true);
|
||||||
};
|
};
|
||||||
@ -86,7 +87,7 @@ export default function SearchSettingsView({
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.put(
|
.put(
|
||||||
`config/set?semantic_search.enabled=${searchSettings.enabled ? "True" : "False"}&semantic_search.reindex=${searchSettings.reindex ? "True" : "False"}&semantic_search.model_size=${searchSettings.model_size}`,
|
`config/set?semantic_search.enabled=${ExploreSettings.enabled ? "True" : "False"}&semantic_search.reindex=${ExploreSettings.reindex ? "True" : "False"}&semantic_search.model_size=${ExploreSettings.model_size}`,
|
||||||
{
|
{
|
||||||
requires_restart: 0,
|
requires_restart: 0,
|
||||||
},
|
},
|
||||||
@ -115,16 +116,16 @@ export default function SearchSettingsView({
|
|||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
updateConfig,
|
updateConfig,
|
||||||
searchSettings.enabled,
|
ExploreSettings.enabled,
|
||||||
searchSettings.reindex,
|
ExploreSettings.reindex,
|
||||||
searchSettings.model_size,
|
ExploreSettings.model_size,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
setSearchSettings(origSearchSettings);
|
setExploreSettings(origExploreSettings);
|
||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
removeMessage("search_settings", "search_settings");
|
removeMessage("search_settings", "search_settings");
|
||||||
}, [origSearchSettings, removeMessage]);
|
}, [origExploreSettings, removeMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (changedValue) {
|
if (changedValue) {
|
||||||
@ -142,7 +143,7 @@ export default function SearchSettingsView({
|
|||||||
}, [changedValue]);
|
}, [changedValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Search Settings - Frigate";
|
document.title = "Explore Settings - Frigate";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -154,16 +155,16 @@ export default function SearchSettingsView({
|
|||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
<Trans>ui.settingView.searchSettings</Trans>
|
<Trans>ui.settingView.exploreSettings</Trans>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Separator className="my-2 flex bg-secondary" />
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
<Heading as="h4" className="my-2">
|
<Heading as="h4" className="my-2">
|
||||||
<Trans>ui.settingView.searchSettings.semanticSearch</Trans>
|
<Trans>ui.settingView.exploreSettings.semanticSearch</Trans>
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||||
<p>
|
<p>
|
||||||
<Trans>ui.settingView.searchSettings.semanticSearch.desc</Trans>
|
<Trans>ui.settingView.exploreSettings.semanticSearch.desc</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center text-primary">
|
<div className="flex items-center text-primary">
|
||||||
@ -174,7 +175,7 @@ export default function SearchSettingsView({
|
|||||||
className="inline"
|
className="inline"
|
||||||
>
|
>
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.readTheDocumentation
|
ui.settingView.exploreSettings.semanticSearch.readTheDocumentation
|
||||||
</Trans>
|
</Trans>
|
||||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -187,8 +188,8 @@ export default function SearchSettingsView({
|
|||||||
<Switch
|
<Switch
|
||||||
id="enabled"
|
id="enabled"
|
||||||
className="mr-3"
|
className="mr-3"
|
||||||
disabled={searchSettings.enabled === undefined}
|
disabled={ExploreSettings.enabled === undefined}
|
||||||
checked={searchSettings.enabled === true}
|
checked={ExploreSettings.enabled === true}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
handleSearchConfigChange({ enabled: isChecked });
|
handleSearchConfigChange({ enabled: isChecked });
|
||||||
}}
|
}}
|
||||||
@ -204,8 +205,8 @@ export default function SearchSettingsView({
|
|||||||
<Switch
|
<Switch
|
||||||
id="reindex"
|
id="reindex"
|
||||||
className="mr-3"
|
className="mr-3"
|
||||||
disabled={searchSettings.reindex === undefined}
|
disabled={ExploreSettings.reindex === undefined}
|
||||||
checked={searchSettings.reindex === true}
|
checked={ExploreSettings.reindex === true}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
handleSearchConfigChange({ reindex: isChecked });
|
handleSearchConfigChange({ reindex: isChecked });
|
||||||
}}
|
}}
|
||||||
@ -213,14 +214,14 @@ export default function SearchSettingsView({
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="reindex">
|
<Label htmlFor="reindex">
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.reindexOnStartup
|
ui.settingView.exploreSettings.semanticSearch.reindexOnStartup
|
||||||
</Trans>
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-sm text-muted-foreground">
|
<div className="mt-3 text-sm text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.reindexOnStartup.desc
|
ui.settingView.exploreSettings.semanticSearch.reindexOnStartup.desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -228,31 +229,31 @@ export default function SearchSettingsView({
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="text-md">
|
<div className="text-md">
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.modelSize
|
ui.settingView.exploreSettings.semanticSearch.modelSize
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
<div className="space-y-1 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.modelSize.desc
|
ui.settingView.exploreSettings.semanticSearch.modelSize.desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-5 text-sm">
|
<ul className="list-disc pl-5 text-sm">
|
||||||
<li>
|
<li>
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.modelSize.small.desc
|
ui.settingView.exploreSettings.semanticSearch.modelSize.small.desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Trans>
|
<Trans>
|
||||||
ui.settingView.searchSettings.semanticSearch.modelSize.large.desc
|
ui.settingView.exploreSettings.semanticSearch.modelSize.large.desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={searchSettings.model_size}
|
value={ExploreSettings.model_size}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleSearchConfigChange({
|
handleSearchConfigChange({
|
||||||
model_size: value as SearchModelSize,
|
model_size: value as SearchModelSize,
|
||||||
@ -261,8 +262,8 @@ export default function SearchSettingsView({
|
|||||||
>
|
>
|
||||||
<SelectTrigger className="w-20">
|
<SelectTrigger className="w-20">
|
||||||
{t(
|
{t(
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize." +
|
"ui.settingView.exploreSettings.semanticSearch.modelSize." +
|
||||||
searchSettings.model_size,
|
ExploreSettings.model_size,
|
||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -274,7 +275,7 @@ export default function SearchSettingsView({
|
|||||||
value={size}
|
value={size}
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
"ui.settingView.searchSettings.semanticSearch.modelSize." +
|
"ui.settingView.exploreSettings.semanticSearch.modelSize." +
|
||||||
size,
|
size,
|
||||||
)}
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user