mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-06 05:27:44 +03:00
chore: add more translation key
This commit is contained in:
parent
e72e27e058
commit
10fa4ec2ca
20
web/package-lock.json
generated
20
web/package-lock.json
generated
@ -5477,15 +5477,6 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/human-signals": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/i18next": {
|
"node_modules/i18next": {
|
||||||
"version": "24.2.0",
|
"version": "24.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz",
|
||||||
@ -8590,7 +8581,7 @@
|
|||||||
"version": "5.8.2",
|
"version": "5.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@ -9016,6 +9007,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vscode-jsonrpc": {
|
"node_modules/vscode-jsonrpc": {
|
||||||
"version": "8.2.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
|||||||
@ -64,6 +64,7 @@
|
|||||||
"info": "Info"
|
"info": "Info"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"system": "System",
|
||||||
"systemMetrics": "System metrics",
|
"systemMetrics": "System metrics",
|
||||||
"configuration": "Configuration",
|
"configuration": "Configuration",
|
||||||
"systemLogs": "System logs",
|
"systemLogs": "System logs",
|
||||||
@ -112,5 +113,11 @@
|
|||||||
"error": "Failed to save config changes: {{errorMessage}}",
|
"error": "Failed to save config changes: {{errorMessage}}",
|
||||||
"error.noMessage": "Failed to save config changes"
|
"error.noMessage": "Failed to save config changes"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"title": "Role",
|
||||||
|
"admin": "Admin",
|
||||||
|
"viewer": "Viewer",
|
||||||
|
"desc": "Admins have full access to all features in the Frigate UI. Viewers are limited to viewing cameras, review items, and historical footage in the UI."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"regenerateFromThumbnails": "Regenerate from Thumbnails",
|
"regenerateFromThumbnails": "Regenerate from Thumbnails",
|
||||||
"tips": {
|
"tips": {
|
||||||
"descriptionSaved": "Successfully saved description",
|
"descriptionSaved": "Successfully saved description",
|
||||||
"saveDescriptionFailed": "Failed to update the description"
|
"saveDescriptionFailed": "Failed to update the description: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"itemMenu": {
|
"itemMenu": {
|
||||||
@ -59,5 +59,6 @@
|
|||||||
"title": "Confirm Delete",
|
"title": "Confirm Delete",
|
||||||
"desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of this tracked object in History view will <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?"
|
"desc": "Deleting this tracked object removes the snapshot, any saved embeddings, and any associated object lifecycle entries. Recorded footage of this tracked object in History view will <em>NOT</em> be deleted.<br /><br />Are you sure you want to proceed?"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"noTrackedObjects": "No Tracked Objects Found"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,6 +91,10 @@
|
|||||||
"noDefinedZones": "No zones are defined for this camera.",
|
"noDefinedZones": "No zones are defined for this camera.",
|
||||||
"objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.",
|
"objectAlertsTips": "All {{alertsLabels}} objects on {{cameraName}} will be shown as Alerts.",
|
||||||
"zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.",
|
"zoneObjectAlertsTips": "All {{alertsLabels}} objects detected in {{zone}} on {{cameraName}} will be shown as Alerts.",
|
||||||
|
"objectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.",
|
||||||
|
"zoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized in {{zone}} on {{cameraName}} will be shown as Detections.",
|
||||||
|
"zoneObjectDetectionsTips.notSelectDetections": "All {{detectionsLabels}} objects detected in {{zone}} on {{cameraName}} not categorized as Alerts will be shown as Detections regardless of which zone they are in.",
|
||||||
|
"zoneObjectDetectionsTips.regardlessOfZoneObjectDetectionsTips": "All {{detectionsLabels}} objects not categorized on {{cameraName}} will be shown as Detections regardless of which zone they are in.",
|
||||||
"selectAlertsZones": "Select zones for Alerts",
|
"selectAlertsZones": "Select zones for Alerts",
|
||||||
"selectDetectionsZones": "Select zones for Detections",
|
"selectDetectionsZones": "Select zones for Detections",
|
||||||
"limitDetections": "Limit detections to specific zones",
|
"limitDetections": "Limit detections to specific zones",
|
||||||
@ -103,9 +107,24 @@
|
|||||||
"filter": {
|
"filter": {
|
||||||
"all": "All Masks and Zones"
|
"all": "All Masks and Zones"
|
||||||
},
|
},
|
||||||
"polygonDrawing": {
|
"form": {
|
||||||
"error": {
|
"zoneName": {
|
||||||
"mustBeFinished": "多边形绘制必须完成闭合后才能保存。"
|
"error": {
|
||||||
|
"mustBeAtLeastTwoCharacters": "Zone name must be at least 2 characters.",
|
||||||
|
"mustNotBeSameWithCamera": "Zone name must not be the same as camera name.",
|
||||||
|
"alreadyExists": "A zone with this name already exists for this camera.",
|
||||||
|
"mustNotContainPeriod": "Zone name must not contain periods.",
|
||||||
|
"hasIllegalCharacter": "Zone name contains illegal characters."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"distance.error": "Distance must be greater than or equal to 0.1.",
|
||||||
|
"distance.error.mustBeFilled": "All distance fields must be filled to use speed estimation.",
|
||||||
|
"inertia.error.mustBeAboveZero": "Inertia must be above 0.",
|
||||||
|
"loiteringTime.error.mustBeGreaterOrEqualZero": "Loitering time must be greater than or equal to 0.",
|
||||||
|
"polygonDrawing": {
|
||||||
|
"error": {
|
||||||
|
"mustBeFinished": "Polygon drawing must be finished before saving."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zones": {
|
"zones": {
|
||||||
@ -182,7 +201,9 @@
|
|||||||
"contourArea.desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. <em>Default: 10</em>",
|
"contourArea.desc": "The contour area value is used to decide which groups of changed pixels qualify as motion. <em>Default: 10</em>",
|
||||||
"improveContrast": "Improve Contrast",
|
"improveContrast": "Improve Contrast",
|
||||||
"improveContrast.desc": "Improve contrast for darker scenes. <em>Default: ON</em>",
|
"improveContrast.desc": "Improve contrast for darker scenes. <em>Default: ON</em>",
|
||||||
"toast.success": "Motion settings have been saved.",
|
"toast": {
|
||||||
|
"success": "Motion settings have been saved."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"title": "Debug",
|
"title": "Debug",
|
||||||
@ -230,29 +251,86 @@
|
|||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Users",
|
"title": "Users",
|
||||||
|
"management": "Users Management",
|
||||||
|
"management.desc": "Manage this Frigate instance's user accounts.",
|
||||||
"addUser": "Add User",
|
"addUser": "Add User",
|
||||||
"updatePassword": "Update Password",
|
"updatePassword": "Update Password",
|
||||||
"toast": {
|
"toast": {
|
||||||
|
"success": {
|
||||||
|
"createUser": "User {{user}} created successfully",
|
||||||
|
"deleteUser": "User {{user}} deleted successfully"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"setPasswordFailed": "Error setting password",
|
"setPasswordFailed": "Failed to save password: {{errorMessage}}",
|
||||||
"createUserFailed": "Error creating user. Check server logs.",
|
"createUserFailed": "Failed to create user: {{errorMessage}}",
|
||||||
"deleteUserFailed": "Error deleting user. Check server logs."
|
"deleteUserFailed": "Failed to delete user: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"table": {
|
||||||
|
"username": "Username",
|
||||||
|
"actions": "Actions",
|
||||||
|
"role": "Role",
|
||||||
|
"noUsers": "No users found.",
|
||||||
|
"changeRole": "Change user role",
|
||||||
|
"password": "Password",
|
||||||
|
"deleteUser": "Delete user"
|
||||||
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
"form": {
|
||||||
|
"user": "Username",
|
||||||
|
"user.desc": "Only letters, numbers, periods and underscores allowed.",
|
||||||
|
"user.placeholder": "Enter username",
|
||||||
|
"password": "Password",
|
||||||
|
"password.placeholder": "Enter password",
|
||||||
|
"password.confirm": "Confirm Password",
|
||||||
|
"password.confirm.placeholder": "Confirm Password",
|
||||||
|
"password.strength": "password strength: ",
|
||||||
|
"password.strength.weak": "Weak",
|
||||||
|
"password.strength.medium": "Medium",
|
||||||
|
"password.strength.strong": "Strong",
|
||||||
|
"password.strength.veryStrong": "Very strong",
|
||||||
|
"password.match": "Passwords match",
|
||||||
|
"password.notMatch": "Passwords don't match",
|
||||||
|
"newPassword": "New Password",
|
||||||
|
"newPassword.placeholder": "Enter new password",
|
||||||
|
"newPassword.confirm.placeholder": "Re-enter new password",
|
||||||
|
"usernameIsRequired": "Username is required"
|
||||||
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "Create User",
|
"title": "Create New User",
|
||||||
"user": "user",
|
"desc": "Add a new user account and specify an role for access to areas of the Frigate UI.",
|
||||||
"password": "password",
|
|
||||||
"usernameOnlyInclude": "Username may only include letters, numbers, . or _"
|
"usernameOnlyInclude": "Username may only include letters, numbers, . or _"
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"title": "Delete User",
|
"title": "Delete User",
|
||||||
"warn": "Are you sure?"
|
"desc": "This action cannot be undone. This will permanently delete the user account and remove all associated data.",
|
||||||
|
"warn": "Are you sure you want to delete <span className=\"font-bold\">{{username}}</span>?"
|
||||||
},
|
},
|
||||||
"setPassword": {
|
"passwordSetting": {
|
||||||
"title": "Set Password"
|
"updatePassword": "Update Password for {{username}}",
|
||||||
|
"setPassword": "Set Password",
|
||||||
|
"desc": "Create a strong password to secure this account."
|
||||||
|
},
|
||||||
|
"changeRole": {
|
||||||
|
"title": "Change User Role",
|
||||||
|
"desc": "Update permissions for <span className=\"font-medium\">{{username}}</span>",
|
||||||
|
"roleInfo": "<p>Select the appropriate role for this user:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">Admin:</span> Full access to all features. </li><li> • <span className=\"font-medium\">Viewer:</span> Limited to Live dashboards, Review, Explore, and Exports only.</li></ul>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"notificationSettings": "Notification Settings",
|
||||||
|
"desc": "Frigate can send native push notifications to your device when running in the browser or installed as a PWA.",
|
||||||
|
"documentation": "Read the Documentation",
|
||||||
|
"email": "Email",
|
||||||
|
"email.placeholder": "e.g. example@email.com",
|
||||||
|
"email.desc": "A valid email is required and will be used to notify you if there are any issues with the push service.",
|
||||||
|
"cameras": "Cameras",
|
||||||
|
"cameras.noCameras": "No cameras available",
|
||||||
|
"cameras.desc": "Select which cameras to enable notifications for.",
|
||||||
|
"deviceSpecific": "Device Specific Settings",
|
||||||
|
"registerDevice": "Register This Device",
|
||||||
|
"unregisterDevice": "Unregister This Device"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,5 +112,11 @@
|
|||||||
"error": "保存配置信息失败: {{errorMessage}}",
|
"error": "保存配置信息失败: {{errorMessage}}",
|
||||||
"error.noMessage": "保存配置信息失败"
|
"error.noMessage": "保存配置信息失败"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"title": "权限组",
|
||||||
|
"admin": "管理员",
|
||||||
|
"viewer": "查看者",
|
||||||
|
"desc": "管理员可以完全访问 Frigate UI 的所有功能。查看者则仅限于在 UI 中查看摄像头、审核项和历史录像。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"regenerateFromThumbnails": "从缩略图重新生成",
|
"regenerateFromThumbnails": "从缩略图重新生成",
|
||||||
"tips": {
|
"tips": {
|
||||||
"descriptionSaved": "已保存描述",
|
"descriptionSaved": "已保存描述",
|
||||||
"saveDescriptionFailed": "更新描述失败"
|
"saveDescriptionFailed": "更新描述失败:{{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"itemMenu": {
|
"itemMenu": {
|
||||||
|
|||||||
@ -158,7 +158,7 @@
|
|||||||
"motionMasks": {
|
"motionMasks": {
|
||||||
"label": "运动遮罩",
|
"label": "运动遮罩",
|
||||||
"documentTitle": "编辑运动遮罩 - Frigate",
|
"documentTitle": "编辑运动遮罩 - Frigate",
|
||||||
"desc": "该功能用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
"desc": "运动遮罩用于防止触发不必要的运动类型。过度的设置遮罩将使对象更加难以被追踪",
|
||||||
"desc.documentation": "文档(英文)",
|
"desc.documentation": "文档(英文)",
|
||||||
"add": "添加运动遮罩",
|
"add": "添加运动遮罩",
|
||||||
"edit": "编辑运动遮罩",
|
"edit": "编辑运动遮罩",
|
||||||
@ -200,7 +200,10 @@
|
|||||||
"contourArea": "轮廓面积",
|
"contourArea": "轮廓面积",
|
||||||
"contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。<em>默认值:10</em>",
|
"contourArea.desc": "轮廓面积决定哪些变化的像素组符合运动条件。<em>默认值:10</em>",
|
||||||
"improveContrast": "提高对比度",
|
"improveContrast": "提高对比度",
|
||||||
"improveContrast.desc": "提高较暗场景的对比度。默认值:开启"
|
"improveContrast.desc": "提高较暗场景的对比度。默认值:开启",
|
||||||
|
"toast": {
|
||||||
|
"success": "运动设置已保存。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"title": "调试",
|
"title": "调试",
|
||||||
@ -248,29 +251,73 @@
|
|||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "用户",
|
"title": "用户",
|
||||||
|
"management": "用户管理",
|
||||||
|
"management.desc": "管理此 Frigate 实例的用户账户。",
|
||||||
"addUser": "添加用户",
|
"addUser": "添加用户",
|
||||||
"updatePassword": "修改密码",
|
"updatePassword": "修改密码",
|
||||||
"toast": {
|
"toast": {
|
||||||
|
"success": {
|
||||||
|
"createUser": "用户 {{user}} 创建成功",
|
||||||
|
"deleteUser": "用户 {{user}} 删除成功"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"setPasswordFailed": "保存密码出现错误",
|
"setPasswordFailed": "保存密码出现错误:{{errorMessage}}",
|
||||||
"createUserFailed": "创建用户失败!请检查后台日志。",
|
"createUserFailed": "创建用户失败:{{errorMessage}}",
|
||||||
"deleteUserFailed": "删除用户失败!请检查后台日志。"
|
"deleteUserFailed": "删除用户失败:{{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"table": {
|
||||||
|
"username": "用户名",
|
||||||
|
"actions": "操作",
|
||||||
|
"role": "权限组",
|
||||||
|
"noUsers": "未找到用户。",
|
||||||
|
"changeRole": "更改用户角色",
|
||||||
|
"password": "密码",
|
||||||
|
"deleteUser": "删除用户"
|
||||||
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
"form": {
|
||||||
|
"user": "用户名",
|
||||||
|
"user.desc": "仅允许使用字母、数字、句点和下划线。",
|
||||||
|
"user.placeholder": "请输入用户名",
|
||||||
|
"password": "密码",
|
||||||
|
"password.placeholder": "请输入密码",
|
||||||
|
"password.confirm": "确认密码",
|
||||||
|
"password.confirm.placeholder": "请再次输入密码",
|
||||||
|
"password.strength": "密码强度:",
|
||||||
|
"password.strength.weak": "弱",
|
||||||
|
"password.strength.medium": "中等",
|
||||||
|
"password.strength.strong": "强",
|
||||||
|
"password.strength.veryStrong": "非常强",
|
||||||
|
"password.match": "密码匹配",
|
||||||
|
"password.notMatch": "密码不匹配",
|
||||||
|
"newPassword": "新密码",
|
||||||
|
"newPassword.placeholder": "请输入新密码",
|
||||||
|
"newPassword.confirm.placeholder": "请再次输入新密码",
|
||||||
|
"usernameIsRequired": "用户名为必填项"
|
||||||
|
},
|
||||||
"createUser": {
|
"createUser": {
|
||||||
"title": "创建用户",
|
"title": "创建新用户",
|
||||||
|
"desc": "创建一个新用户账户,并指定一个角色以控制访问 Frigate UI 的权限。",
|
||||||
"user": "用户",
|
"user": "用户",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"usernameOnlyInclude": "用户名只能包含字母、数字和 _"
|
"usernameOnlyInclude": "用户名只能包含字母、数字和 _"
|
||||||
},
|
},
|
||||||
"deleteUser": {
|
"deleteUser": {
|
||||||
"title": "删除该用户",
|
"title": "删除该用户",
|
||||||
"warn": "你确定要删除该用户吗?"
|
"desc": "此操作无法撤销。这将永久删除用户账户并移除所有相关数据。",
|
||||||
|
"warn": "你确定要删除 <span className=\"font-bold\">{{username}}</span> 吗?"
|
||||||
},
|
},
|
||||||
"setPassword": {
|
"passwordSetting": {
|
||||||
"title": "修改密码"
|
"updatePassword": "更新 {{username}} 的密码",
|
||||||
}
|
"setPassword": "设置密码",
|
||||||
|
"desc": "创建一个强密码来保护此账户。"
|
||||||
|
},
|
||||||
|
"changeRole": {
|
||||||
|
"title": "更改用户权限组",
|
||||||
|
"desc": "更新 <span className=\"font-medium\">{{username}}</span> 的权限",
|
||||||
|
"roleInfo": "<p>请选择此用户的适当角色:</p><ul className=\"mt-2 space-y-1 pl-5\"><li> • <span className=\"font-medium\">管理员 (Admin):</span> 拥有所有功能的完整访问权限。</li><li> • <span className=\"font-medium\">查看者 (Viewer):</span> 仅限访问实时监控、回放、探测和导出功能。</li></ul>"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
|
|||||||
@ -83,9 +83,12 @@ export default function ReviewCard({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(t("export.toast.success", { ns: "components/dialog"}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("export.toast.success", { ns: "components/dialog" }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@ -66,12 +66,9 @@ const timeAgo = ({
|
|||||||
if (monthDiff > 0) {
|
if (monthDiff > 0) {
|
||||||
const unitAmount = monthDiff;
|
const unitAmount = monthDiff;
|
||||||
return t("time.ago", {
|
return t("time.ago", {
|
||||||
timeAgo: t(
|
timeAgo: t(`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`, {
|
||||||
`time.${dense ? timeUnits[i].unit : timeUnits[i].full}`,
|
time: unitAmount,
|
||||||
{
|
}),
|
||||||
time: unitAmount,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (elapsed >= timeUnits[i].value) {
|
} else if (elapsed >= timeUnits[i].value) {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function CalendarFilterButton({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const selectedDate = useFormattedTimestamp(
|
const selectedDate = useFormattedTimestamp(
|
||||||
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
day == undefined ? 0 : day?.getTime() / 1000 + 1,
|
||||||
"%b %-d",
|
t("time.formattedTimestampOnlyMonthAndDay"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const trigger = (
|
const trigger = (
|
||||||
@ -48,7 +48,9 @@ export default function CalendarFilterButton({
|
|||||||
<div
|
<div
|
||||||
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
|
className={`hidden md:block ${day == undefined ? "text-primary" : "text-selected-foreground"}`}
|
||||||
>
|
>
|
||||||
{day == undefined ? t("calendarFilter.last24Hours", {ns: "views/events"}) : selectedDate}
|
{day == undefined
|
||||||
|
? t("calendarFilter.last24Hours", { ns: "views/events" })
|
||||||
|
: selectedDate}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -656,7 +656,9 @@ export function CameraGroupEdit({
|
|||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(2, {
|
.min(2, {
|
||||||
message: t("group.name.errorMessage.mustLeastCharacters", { ns: "components/camera" }),
|
message: t("group.name.errorMessage.mustLeastCharacters", {
|
||||||
|
ns: "components/camera",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||||
.refine(
|
.refine(
|
||||||
@ -667,7 +669,9 @@ export function CameraGroupEdit({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t("group.name.errorMessage.exists", { ns: "components/camera" }),
|
message: t("group.name.errorMessage.exists", {
|
||||||
|
ns: "components/camera",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@ -679,7 +683,9 @@ export function CameraGroupEdit({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine((value: string) => value.toLowerCase() !== "default", {
|
.refine((value: string) => value.toLowerCase() !== "default", {
|
||||||
message: t("group.name.errorMessage.invalid", { ns: "components/camera" }),
|
message: t("group.name.errorMessage.invalid", {
|
||||||
|
ns: "components/camera",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cameras: z.array(z.string()),
|
cameras: z.array(z.string()),
|
||||||
@ -735,7 +741,10 @@ export function CameraGroupEdit({
|
|||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(
|
toast.success(
|
||||||
t("group.toast.success", { name: values.name, ns: "components/camera"}),
|
t("group.toast.success", {
|
||||||
|
name: values.name,
|
||||||
|
ns: "components/camera",
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
},
|
},
|
||||||
@ -756,9 +765,9 @@ export function CameraGroupEdit({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(
|
toast.error(
|
||||||
t("toast.save.error", {
|
t("toast.save.error", {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -809,7 +818,9 @@ export function CameraGroupEdit({
|
|||||||
<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={t("group.name.placeholder", { ns: "components/camera" })}
|
placeholder={t("group.name.placeholder", {
|
||||||
|
ns: "components/camera",
|
||||||
|
})}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -153,7 +153,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={t("cameras.all", { ns: "components/filter"})}
|
label={t("cameras.all", { ns: "components/filter" })}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
setCurrentCameras(undefined);
|
setCurrentCameras(undefined);
|
||||||
|
|||||||
@ -198,7 +198,9 @@ export default function SearchFilterGroup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultText={
|
defaultText={
|
||||||
isMobile ? t("dates.all.short", {ns: "components/filter"}) : t("dates.all", {ns: "components/filter"})
|
isMobile
|
||||||
|
? t("dates.all.short", { ns: "components/filter" })
|
||||||
|
: t("dates.all", { ns: "components/filter" })
|
||||||
}
|
}
|
||||||
updateSelectedRange={onUpdateSelectedRange}
|
updateSelectedRange={onUpdateSelectedRange}
|
||||||
/>
|
/>
|
||||||
@ -240,18 +242,21 @@ function GeneralFilterButton({
|
|||||||
|
|
||||||
const buttonText = useMemo(() => {
|
const buttonText = useMemo(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return t("labels.all.short", {ns: "components/filter"});
|
return t("labels.all.short", { ns: "components/filter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedLabels || selectedLabels.length == 0) {
|
if (!selectedLabels || selectedLabels.length == 0) {
|
||||||
return t("labels.all", {ns: "components/filter"});
|
return t("labels.all", { ns: "components/filter" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLabels.length == 1) {
|
if (selectedLabels.length == 1) {
|
||||||
return t(selectedLabels[0], { ns: "objects"});
|
return t(selectedLabels[0], { ns: "objects" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return t("labels.count", { count: selectedLabels.length, ns: "components/filter" });
|
return t("labels.count", {
|
||||||
|
count: selectedLabels.length,
|
||||||
|
ns: "components/filter",
|
||||||
|
});
|
||||||
}, [selectedLabels]);
|
}, [selectedLabels]);
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
@ -352,7 +357,7 @@ export function GeneralFilterContent({
|
|||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
label={t(item, {ns: "objects"})}
|
label={t(item, { ns: "objects" })}
|
||||||
isChecked={currentLabels?.includes(item) ?? false}
|
isChecked={currentLabels?.includes(item) ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
@ -501,13 +506,13 @@ export function SortTypeContent({
|
|||||||
onClose,
|
onClose,
|
||||||
}: SortTypeContentProps) {
|
}: SortTypeContentProps) {
|
||||||
const sortLabels = {
|
const sortLabels = {
|
||||||
date_asc: t("sort.dateAsc", {ns: "components/filter"}),
|
date_asc: t("sort.dateAsc", { ns: "components/filter" }),
|
||||||
date_desc: t("sort.dateDesc", {ns: "components/filter"}),
|
date_desc: t("sort.dateDesc", { ns: "components/filter" }),
|
||||||
score_asc: t("sort.scoreAsc", {ns: "components/filter"}),
|
score_asc: t("sort.scoreAsc", { ns: "components/filter" }),
|
||||||
score_desc: t("sort.scoreDesc", {ns: "components/filter"}),
|
score_desc: t("sort.scoreDesc", { ns: "components/filter" }),
|
||||||
speed_asc: t("sort.speedAsc", {ns: "components/filter"}),
|
speed_asc: t("sort.speedAsc", { ns: "components/filter" }),
|
||||||
speed_desc: t("sort.speedDesc", {ns: "components/filter"}),
|
speed_desc: t("sort.speedDesc", { ns: "components/filter" }),
|
||||||
relevance: t("sort.relevance", {ns: "components/filter"}),
|
relevance: t("sort.relevance", { ns: "components/filter" }),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export function CameraLineGraph({
|
|||||||
style={{ color: GRAPH_COLORS[labelIdx] }}
|
style={{ color: GRAPH_COLORS[labelIdx] }}
|
||||||
/>
|
/>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{t("cameras.label." + label, {ns: "views/settings"})}
|
{t("cameras.label." + label, { ns: "views/settings" })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-primary">
|
<div className="text-xs text-primary">
|
||||||
{lastValues[labelIdx]}
|
{lastValues[labelIdx]}
|
||||||
|
|||||||
@ -182,7 +182,9 @@ export function CombinedStorageGraph({
|
|||||||
<Trans ns="views/system">storage.cameraStorage.camera</Trans>
|
<Trans ns="views/system">storage.cameraStorage.camera</Trans>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<Trans ns="views/system">storage.cameraStorage.storageUsed</Trans>
|
<Trans ns="views/system">
|
||||||
|
storage.cameraStorage.storageUsed
|
||||||
|
</Trans>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<Trans ns="views/system">
|
<Trans ns="views/system">
|
||||||
@ -205,8 +207,8 @@ export function CombinedStorageGraph({
|
|||||||
></div>
|
></div>
|
||||||
{item.name === "Unused"
|
{item.name === "Unused"
|
||||||
? t("storage.cameraStorage.unused", {
|
? t("storage.cameraStorage.unused", {
|
||||||
ns: "views/system",
|
ns: "views/system",
|
||||||
})
|
})
|
||||||
: item.name.replaceAll("_", " ")}
|
: item.name.replaceAll("_", " ")}
|
||||||
{item.name === "Unused" && (
|
{item.name === "Unused" && (
|
||||||
<Popover>
|
<Popover>
|
||||||
|
|||||||
@ -117,7 +117,9 @@ export default function IconPicker({
|
|||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("iconPicker.search.placeholder", {ns: "components/icons"})}
|
placeholder={t("iconPicker.search.placeholder", {
|
||||||
|
ns: "components/icons",
|
||||||
|
})}
|
||||||
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)}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export function SaveSearchDialog({
|
|||||||
toast.success(
|
toast.success(
|
||||||
t("search.saveSearch.success", {
|
t("search.saveSearch.success", {
|
||||||
searchName: searchName.trim(),
|
searchName: searchName.trim(),
|
||||||
ns: "components/dialog"
|
ns: "components/dialog",
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
@ -73,7 +73,9 @@ export function SaveSearchDialog({
|
|||||||
value={searchName}
|
value={searchName}
|
||||||
className="text-md"
|
className="text-md"
|
||||||
onChange={(e) => setSearchName(e.target.value)}
|
onChange={(e) => setSearchName(e.target.value)}
|
||||||
placeholder={t("search.saveSearch.placeholder", {ns: "components/dialog"})}
|
placeholder={t("search.saveSearch.placeholder", {
|
||||||
|
ns: "components/dialog",
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{overwrite && (
|
{overwrite && (
|
||||||
<div className="ml-1 text-sm text-danger">
|
<div className="ml-1 text-sm text-danger">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
LuSettings,
|
LuSettings,
|
||||||
LuSun,
|
LuSun,
|
||||||
LuSunMoon,
|
LuSunMoon,
|
||||||
|
LuEarth,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -195,7 +196,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel><Trans>menu.system</Trans></DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
<Trans>menu.system</Trans>
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
|
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
|
||||||
<Link to="/system#general">
|
<Link to="/system#general">
|
||||||
@ -208,7 +211,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
aria-label="System metrics"
|
aria-label="System metrics"
|
||||||
>
|
>
|
||||||
<LuActivity className="mr-2 size-4" />
|
<LuActivity className="mr-2 size-4" />
|
||||||
<span><Trans>menu.systemMetrics</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.systemMetrics</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/logs">
|
<Link to="/logs">
|
||||||
@ -221,7 +226,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
aria-label="System logs"
|
aria-label="System logs"
|
||||||
>
|
>
|
||||||
<LuList className="mr-2 size-4" />
|
<LuList className="mr-2 size-4" />
|
||||||
<span><Trans>menu.systemLogs</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.systemLogs</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
@ -261,7 +268,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
aria-label="Configuration editor"
|
aria-label="Configuration editor"
|
||||||
>
|
>
|
||||||
<LuSquarePen className="mr-2 size-4" />
|
<LuSquarePen className="mr-2 size-4" />
|
||||||
<span><Trans>menu.configurationEditor</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.configurationEditor</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@ -271,6 +280,87 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
<Trans>menu.appearance</Trans>
|
<Trans>menu.appearance</Trans>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<SubItem>
|
||||||
|
<SubItemTrigger
|
||||||
|
className={
|
||||||
|
isDesktop ? "cursor-pointer" : "flex items-center p-2 text-sm"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LuLanguages className="mr-2 size-4" />
|
||||||
|
<span>
|
||||||
|
<Trans>menu.languages</Trans>
|
||||||
|
</span>
|
||||||
|
</SubItemTrigger>
|
||||||
|
<Portal>
|
||||||
|
<SubItemContent
|
||||||
|
className={
|
||||||
|
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span tabIndex={0} className="sr-only" />
|
||||||
|
<MenuItem
|
||||||
|
className={
|
||||||
|
isDesktop
|
||||||
|
? "cursor-pointer"
|
||||||
|
: "flex items-center p-2 text-sm"
|
||||||
|
}
|
||||||
|
aria-label="Light mode"
|
||||||
|
onClick={() => setLanguage("en")}
|
||||||
|
>
|
||||||
|
{language.trim() === "en" ? (
|
||||||
|
<>
|
||||||
|
<LuLanguages className="mr-2 size-4" />
|
||||||
|
<Trans>menu.language.en</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.language.en</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className={
|
||||||
|
isDesktop
|
||||||
|
? "cursor-pointer"
|
||||||
|
: "flex items-center p-2 text-sm"
|
||||||
|
}
|
||||||
|
aria-label="Dark mode"
|
||||||
|
onClick={() => setLanguage("zh-CN")}
|
||||||
|
>
|
||||||
|
{language === "zh-CN" ? (
|
||||||
|
<>
|
||||||
|
<LuLanguages className="mr-2 size-4" />
|
||||||
|
<Trans>menu.language.zhCN</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.language.zhCN</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
className={
|
||||||
|
isDesktop
|
||||||
|
? "cursor-pointer"
|
||||||
|
: "flex items-center p-2 text-sm"
|
||||||
|
}
|
||||||
|
aria-label="Use the system settings for light or dark mode"
|
||||||
|
onClick={() => setLanguage(systemLanguage)}
|
||||||
|
>
|
||||||
|
{language === systemLanguage ? (
|
||||||
|
<>
|
||||||
|
<LuEarth className="mr-2 size-4 scale-100 transition-all" />
|
||||||
|
<Trans>menu.withSystem</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.withSystem</Trans>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
</SubItemContent>
|
||||||
|
</Portal>
|
||||||
|
</SubItem>
|
||||||
<SubItem>
|
<SubItem>
|
||||||
<SubItemTrigger
|
<SubItemTrigger
|
||||||
className={
|
className={
|
||||||
@ -278,7 +368,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LuSunMoon className="mr-2 size-4" />
|
<LuSunMoon className="mr-2 size-4" />
|
||||||
<span><Trans>menu.darkMode.label</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.darkMode.label</Trans>
|
||||||
|
</span>
|
||||||
</SubItemTrigger>
|
</SubItemTrigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<SubItemContent
|
<SubItemContent
|
||||||
@ -302,7 +394,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
<Trans>menu.darkMode.light</Trans>
|
<Trans>menu.darkMode.light</Trans>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="ml-6 mr-2"><Trans>menu.darkMode.light</Trans></span>
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.darkMode.light</Trans>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -320,7 +414,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
<Trans>menu.darkMode.dark</Trans>
|
<Trans>menu.darkMode.dark</Trans>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="ml-6 mr-2"><Trans>menu.darkMode.dark</Trans></span>
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.darkMode.dark</Trans>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -338,7 +434,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
<Trans>menu.withSystem</Trans>
|
<Trans>menu.withSystem</Trans>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="ml-6 mr-2"><Trans>menu.withSystem</Trans></span>
|
<span className="ml-6 mr-2">
|
||||||
|
<Trans>menu.withSystem</Trans>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</SubItemContent>
|
</SubItemContent>
|
||||||
@ -351,7 +449,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LuSunMoon className="mr-2 size-4" />
|
<LuSunMoon className="mr-2 size-4" />
|
||||||
<span><Trans>menu.theme.label</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.theme.label</Trans>
|
||||||
|
</span>
|
||||||
</SubItemTrigger>
|
</SubItemTrigger>
|
||||||
<Portal>
|
<Portal>
|
||||||
<SubItemContent
|
<SubItemContent
|
||||||
@ -432,7 +532,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
onClick={() => setRestartDialogOpen(true)}
|
onClick={() => setRestartDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<LuRotateCw className="mr-2 size-4" />
|
<LuRotateCw className="mr-2 size-4" />
|
||||||
<span><Trans>menu.restart</Trans></span>
|
<span>
|
||||||
|
<Trans>menu.restart</Trans>
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -242,7 +242,10 @@ export default function LiveContextMenu({
|
|||||||
time_style: "medium",
|
time_style: "medium",
|
||||||
date_style: "medium",
|
date_style: "medium",
|
||||||
timezone: config?.ui.timezone,
|
timezone: config?.ui.timezone,
|
||||||
strftime_fmt: config?.ui.time_format == "24hour" ? t("time.formattedTimestampExcludeSeconds.24hour"): t("time.formattedTimestampExcludeSeconds"),
|
strftime_fmt:
|
||||||
|
config?.ui.time_format == "24hour"
|
||||||
|
? t("time.formattedTimestampExcludeSeconds.24hour")
|
||||||
|
: t("time.formattedTimestampExcludeSeconds"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -359,7 +362,9 @@ export default function LiveContextMenu({
|
|||||||
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
className="flex w-full cursor-pointer items-center justify-start gap-2"
|
||||||
onClick={isEnabled ? resetPreferredLiveMode : undefined}
|
onClick={isEnabled ? resetPreferredLiveMode : undefined}
|
||||||
>
|
>
|
||||||
<div className="text-primary"><Trans>button</Trans></div>
|
<div className="text-primary">
|
||||||
|
<Trans>button</Trans>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -92,7 +92,9 @@ export default function SearchResultActions({
|
|||||||
const menuItems = (
|
const menuItems = (
|
||||||
<>
|
<>
|
||||||
{searchResult.has_clip && (
|
{searchResult.has_clip && (
|
||||||
<MenuItem aria-label={t("itemMenu.downloadVideo.aria", {ns: "views/explore"})}>
|
<MenuItem
|
||||||
|
aria-label={t("itemMenu.downloadVideo.aria", { ns: "views/explore" })}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
href={`${baseUrl}api/events/${searchResult.id}/clip.mp4`}
|
||||||
@ -107,7 +109,9 @@ export default function SearchResultActions({
|
|||||||
)}
|
)}
|
||||||
{searchResult.has_snapshot && (
|
{searchResult.has_snapshot && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.downloadSnapshot.aria", {ns: "views/explore"})}
|
aria-label={t("itemMenu.downloadSnapshot.aria", {
|
||||||
|
ns: "views/explore",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
@ -123,7 +127,9 @@ export default function SearchResultActions({
|
|||||||
)}
|
)}
|
||||||
{searchResult.data.type == "object" && (
|
{searchResult.data.type == "object" && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.viewObjectLifecycle.aria", {ns: "views/explore"})}
|
aria-label={t("itemMenu.viewObjectLifecycle.aria", {
|
||||||
|
ns: "views/explore",
|
||||||
|
})}
|
||||||
onClick={showObjectLifecycle}
|
onClick={showObjectLifecycle}
|
||||||
>
|
>
|
||||||
<FaArrowsRotate className="mr-2 size-4" />
|
<FaArrowsRotate className="mr-2 size-4" />
|
||||||
@ -134,7 +140,7 @@ export default function SearchResultActions({
|
|||||||
)}
|
)}
|
||||||
{config?.semantic_search?.enabled && isContextMenu && (
|
{config?.semantic_search?.enabled && isContextMenu && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.findSimilar.aria", {ns: "views/explore"})}
|
aria-label={t("itemMenu.findSimilar.aria", { ns: "views/explore" })}
|
||||||
onClick={findSimilar}
|
onClick={findSimilar}
|
||||||
>
|
>
|
||||||
<MdImageSearch className="mr-2 size-4" />
|
<MdImageSearch className="mr-2 size-4" />
|
||||||
@ -150,7 +156,9 @@ export default function SearchResultActions({
|
|||||||
searchResult.data.type == "object" &&
|
searchResult.data.type == "object" &&
|
||||||
!searchResult.plus_id && (
|
!searchResult.plus_id && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.submitToPlus.aria", {ns: "views/explore"})}
|
aria-label={t("itemMenu.submitToPlus.aria", {
|
||||||
|
ns: "views/explore",
|
||||||
|
})}
|
||||||
onClick={showSnapshot}
|
onClick={showSnapshot}
|
||||||
>
|
>
|
||||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||||
@ -215,7 +223,9 @@ export default function SearchResultActions({
|
|||||||
onClick={findSimilar}
|
onClick={findSimilar}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><Trans ns="views/explore">itemMenu.findSimilar.label</Trans></TooltipContent>
|
<TooltipContent>
|
||||||
|
<Trans ns="views/explore">itemMenu.findSimilar.label</Trans>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -232,7 +242,9 @@ export default function SearchResultActions({
|
|||||||
onClick={showSnapshot}
|
onClick={showSnapshot}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent><Trans ns="views/explore">itemMenu.submitToPlus.label</Trans></TooltipContent>
|
<TooltipContent>
|
||||||
|
<Trans ns="views/explore">itemMenu.submitToPlus.label</Trans>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -74,9 +74,9 @@ export default function CameraInfoDialog({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="capitalize">
|
<DialogTitle className="capitalize">
|
||||||
{t("system.cameras.info.cameraProbeInfo", {
|
{t("cameras.info.cameraProbeInfo", {
|
||||||
camera: camera.name.replaceAll("_", " "),
|
camera: camera.name.replaceAll("_", " "),
|
||||||
ns: "views/system"
|
ns: "views/system",
|
||||||
})}
|
})}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -90,7 +90,10 @@ export default function CameraInfoDialog({
|
|||||||
{ffprobeInfo.map((stream, idx) => (
|
{ffprobeInfo.map((stream, idx) => (
|
||||||
<div key={idx} className="mb-5">
|
<div key={idx} className="mb-5">
|
||||||
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
|
<div className="mb-1 rounded-md bg-secondary p-2 text-lg text-primary">
|
||||||
{t("cameras.info.stream", { idx: idx + 1, ns: "views/system" })}
|
{t("cameras.info.stream", {
|
||||||
|
idx: idx + 1,
|
||||||
|
ns: "views/system",
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{stream.return_code == 0 ? (
|
{stream.return_code == 0 ? (
|
||||||
<div>
|
<div>
|
||||||
@ -99,11 +102,15 @@ export default function CameraInfoDialog({
|
|||||||
{codec.width ? (
|
{codec.width ? (
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<Trans ns="views/system">cameras.info.video</Trans>
|
<Trans ns="views/system">
|
||||||
|
cameras.info.video
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-5">
|
<div className="ml-5">
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="views/system">cameras.info.codec</Trans>
|
<Trans ns="views/system">
|
||||||
|
cameras.info.codec
|
||||||
|
</Trans>
|
||||||
<span className="text-primary">
|
<span className="text-primary">
|
||||||
{" "}
|
{" "}
|
||||||
{codec.codec_long_name}
|
{codec.codec_long_name}
|
||||||
@ -138,10 +145,14 @@ export default function CameraInfoDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="views/system">cameras.info.fps</Trans>{" "}
|
<Trans ns="views/system">
|
||||||
|
cameras.info.fps
|
||||||
|
</Trans>{" "}
|
||||||
<span className="text-primary">
|
<span className="text-primary">
|
||||||
{codec.avg_frame_rate == "0/0"
|
{codec.avg_frame_rate == "0/0"
|
||||||
? t("cameras.info.unknown", { ns: "views/system" })
|
? t("cameras.info.unknown", {
|
||||||
|
ns: "views/system",
|
||||||
|
})
|
||||||
: codec.avg_frame_rate}
|
: codec.avg_frame_rate}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +162,9 @@ export default function CameraInfoDialog({
|
|||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
<div className="ml-2 mt-1">Audio:</div>
|
<div className="ml-2 mt-1">Audio:</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
<Trans ns="views/system">cameras.info.codec</Trans>{" "}
|
<Trans ns="views/system">
|
||||||
|
cameras.info.codec
|
||||||
|
</Trans>{" "}
|
||||||
<span className="text-primary">
|
<span className="text-primary">
|
||||||
{codec.codec_long_name}
|
{codec.codec_long_name}
|
||||||
</span>
|
</span>
|
||||||
@ -166,7 +179,7 @@ export default function CameraInfoDialog({
|
|||||||
<div>
|
<div>
|
||||||
{t("cameras.info.error", {
|
{t("cameras.info.error", {
|
||||||
error: stream.stderr,
|
error: stream.stderr,
|
||||||
ns: "views/system"
|
ns: "views/system",
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,16 +51,22 @@ export default function CreateUserDialog({
|
|||||||
.object({
|
.object({
|
||||||
user: z
|
user: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Username is required")
|
.min(1, t("users.dialog.form.usernameIsRequired", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}))
|
||||||
.regex(/^[A-Za-z0-9._]+$/, {
|
.regex(/^[A-Za-z0-9._]+$/, {
|
||||||
message: t("users.dialog.createUser.usernameOnlyInclude", {ns: "views/settings"}),
|
message: t("users.dialog.createUser.usernameOnlyInclude", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
password: z.string().min(1, "Password is required"),
|
password: z.string().min(1, "Password is required"),
|
||||||
confirmPassword: z.string().min(1, "Please confirm your password"),
|
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||||
role: z.enum(["admin", "viewer"]),
|
role: z.enum(["admin", "viewer"]),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
message: "Passwords don't match",
|
message: t("users.dialog.form.password.notMatch", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
path: ["confirmPassword"],
|
path: ["confirmPassword"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,9 +117,11 @@ export default function CreateUserDialog({
|
|||||||
<Dialog open={show} onOpenChange={onCancel}>
|
<Dialog open={show} onOpenChange={onCancel}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle><Trans ns="views/settings">users.dialog.createUser.title</Trans></DialogTitle>
|
<DialogTitle>
|
||||||
|
<Trans ns="views/settings">users.dialog.createUser.title</Trans>
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans ns="views/settings">users.dialog.createUser.desc</Trans>
|
<Trans ns="views/settings">users.dialog.createUser.desc</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -127,17 +135,21 @@ export default function CreateUserDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">
|
<FormLabel className="text-sm font-medium">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.user</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.user
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter username"
|
placeholder={t("users.dialog.form.user.placeholder", { ns: "views/settings" })}
|
||||||
className="h-10"
|
className="h-10"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription className="text-xs text-muted-foreground">
|
<FormDescription className="text-xs text-muted-foreground">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.user.desc</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.user.desc
|
||||||
|
</Trans>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -149,11 +161,13 @@ export default function CreateUserDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">
|
<FormLabel className="text-sm font-medium">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.password</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter password"
|
placeholder={t("users.dialog.form.password.placeholder", { ns: "views/settings" })}
|
||||||
type="password"
|
type="password"
|
||||||
className="h-10"
|
className="h-10"
|
||||||
{...field}
|
{...field}
|
||||||
@ -169,11 +183,13 @@ export default function CreateUserDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">
|
<FormLabel className="text-sm font-medium">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.confirmPassword</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.confirm
|
||||||
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Confirm password"
|
placeholder={t("users.dialog.form.password.confirm.placeholder", { ns: "views/settings" })}
|
||||||
type="password"
|
type="password"
|
||||||
className="h-10"
|
className="h-10"
|
||||||
{...field}
|
{...field}
|
||||||
@ -185,14 +201,18 @@ export default function CreateUserDialog({
|
|||||||
<>
|
<>
|
||||||
<LuCheck className="size-3.5 text-green-500" />
|
<LuCheck className="size-3.5 text-green-500" />
|
||||||
<span className="text-green-600">
|
<span className="text-green-600">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.password.match</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.match
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LuX className="size-3.5 text-red-500" />
|
<LuX className="size-3.5 text-red-500" />
|
||||||
<span className="text-red-600">
|
<span className="text-red-600">
|
||||||
<Trans ns="views/settings">users.dialog.createUser.password.notMatch</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.notMatch
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -207,7 +227,9 @@ export default function CreateUserDialog({
|
|||||||
name="role"
|
name="role"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium"><Trans>role.title</Trans></FormLabel>
|
<FormLabel className="text-sm font-medium">
|
||||||
|
<Trans>role.title</Trans>
|
||||||
|
</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
@ -224,7 +246,9 @@ export default function CreateUserDialog({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="h-4 w-4 text-primary" />
|
<Shield className="h-4 w-4 text-primary" />
|
||||||
<span><Trans>role.admin</Trans></span>
|
<span>
|
||||||
|
<Trans>role.admin</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@ -233,7 +257,9 @@ export default function CreateUserDialog({
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
<User className="h-4 w-4 text-muted-foreground" />
|
||||||
<span><Trans>role.viewer</Trans></span>
|
<span>
|
||||||
|
<Trans>role.viewer</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -256,7 +282,7 @@ export default function CreateUserDialog({
|
|||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>button.cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
@ -268,7 +294,9 @@ export default function CreateUserDialog({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
<span><Trans>button.saving</Trans></span>
|
<span>
|
||||||
|
<Trans>button.saving</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Trans>button.save</Trans>
|
<Trans>button.save</Trans>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
|
||||||
} from "../ui/dialog";
|
} from "../ui/dialog";
|
||||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||||
|
|
||||||
@ -35,7 +34,9 @@ export default function DeleteUserDialog({
|
|||||||
|
|
||||||
<div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm">
|
<div className="my-4 rounded-md border border-destructive/20 bg-destructive/5 p-4 text-center text-sm">
|
||||||
<p className="font-medium text-destructive">
|
<p className="font-medium text-destructive">
|
||||||
<Trans ns="views/settings" values={{username}}>users.dialog.deleteUser.warn</Trans>
|
<Trans ns="views/settings" values={{ username }}>
|
||||||
|
users.dialog.deleteUser.warn
|
||||||
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -70,16 +70,26 @@ export default function ExportDialog({
|
|||||||
|
|
||||||
const onStartExport = useCallback(() => {
|
const onStartExport = useCallback(() => {
|
||||||
if (!range) {
|
if (!range) {
|
||||||
toast.error(t("export.toast.error.noVaildTimeSelected", {ns: "components/dialog"}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("export.toast.error.noVaildTimeSelected", {
|
||||||
});
|
ns: "components/dialog",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.before < range.after) {
|
if (range.before < range.after) {
|
||||||
toast.error(t("export.toast.error.endTimeMustAfterStartTime", {ns: "components/dialog"}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("export.toast.error.endTimeMustAfterStartTime", {
|
||||||
});
|
ns: "components/dialog",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +103,12 @@ export default function ExportDialog({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(t("export.toast.success", {ns: "components/dialog"}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("export.toast.success", { ns: "components/dialog" }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
setName("");
|
setName("");
|
||||||
setRange(undefined);
|
setRange(undefined);
|
||||||
setMode("none");
|
setMode("none");
|
||||||
@ -106,10 +119,13 @@ export default function ExportDialog({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(
|
toast.error(
|
||||||
t("export.toast.error.failed", { error: errorMessage, ns: "components/dialog" }),
|
t("export.toast.error.failed", {
|
||||||
{ position: "top-center" },
|
error: errorMessage,
|
||||||
);
|
ns: "components/dialog",
|
||||||
|
}),
|
||||||
|
{ position: "top-center" },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [camera, name, range, setRange, setName, setMode]);
|
}, [camera, name, range, setRange, setName, setMode]);
|
||||||
|
|
||||||
@ -290,11 +306,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"
|
||||||
? t("export.time.fromTimeline", {ns: "components/dialog"})
|
? t("export.time.fromTimeline", { ns: "components/dialog" })
|
||||||
: t("export.time." + opt, {ns: "components/dialog"})
|
: t("export.time." + opt, { ns: "components/dialog" })
|
||||||
: t("export.time.lastHour", {
|
: t("export.time.lastHour", {
|
||||||
count: parseInt(opt),
|
count: parseInt(opt),
|
||||||
ns: "components/dialog"
|
ns: "components/dialog",
|
||||||
})}
|
})}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@ -311,7 +327,7 @@ export function ExportContent({
|
|||||||
<Input
|
<Input
|
||||||
className="text-md my-6"
|
className="text-md my-6"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={t("export.name.placeholder", {ns: "components/dialog"})}
|
placeholder={t("export.name.placeholder", { ns: "components/dialog" })}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -342,8 +358,8 @@ export function ExportContent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedOption == "timeline"
|
{selectedOption == "timeline"
|
||||||
? t("export.select", {ns: "components/dialog"})
|
? t("export.select", { ns: "components/dialog" })
|
||||||
: t("export.export", {ns: "components/dialog"})}
|
: t("export.export", { ns: "components/dialog" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
@ -598,10 +614,14 @@ export function ExportPreviewDialog({
|
|||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
export.fromTimeline.previewExport
|
||||||
|
</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
export.fromTimeline.previewExport
|
||||||
|
</Trans>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<GenericVideoPlayer source={source} />
|
<GenericVideoPlayer source={source} />
|
||||||
|
|||||||
@ -98,9 +98,12 @@ export default function MobileReviewSettingsDrawer({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(t("export.toast.success", { ns: "components/dialog"}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("export.toast.success", { ns: "components/dialog" }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
setName("");
|
setName("");
|
||||||
setRange(undefined);
|
setRange(undefined);
|
||||||
setMode("none");
|
setMode("none");
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Trans } from "react-i18next";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -41,27 +42,16 @@ export default function RoleChangeDialog({
|
|||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl font-semibold">
|
<DialogTitle className="text-xl font-semibold">
|
||||||
Change User Role
|
<Trans ns="views/settings">users.dialog.changeRole.title</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Update permissions for{" "}
|
<Trans ns="views/settings" values={{username}}>users.dialog.changeRole.desc</Trans>
|
||||||
<span className="font-medium">{username}</span>
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="py-6">
|
<div className="py-6">
|
||||||
<div className="mb-4 text-sm text-muted-foreground">
|
<div className="mb-4 text-sm text-muted-foreground">
|
||||||
<p>Select the appropriate role for this user:</p>
|
<Trans ns="views/settings">users.dialog.changeRole.roleInfo</Trans>
|
||||||
<ul className="mt-2 space-y-1 pl-5">
|
|
||||||
<li>
|
|
||||||
• <span className="font-medium">Admin:</span> Full access to all
|
|
||||||
features.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
• <span className="font-medium">Viewer:</span> Limited to Live
|
|
||||||
dashboards, Review, Explore, and Exports only.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
@ -77,13 +67,13 @@ export default function RoleChangeDialog({
|
|||||||
<SelectItem value="admin" className="flex items-center gap-2">
|
<SelectItem value="admin" className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LuShield className="size-4 text-primary" />
|
<LuShield className="size-4 text-primary" />
|
||||||
<span>Admin</span>
|
<span><Trans>role.admin</Trans></span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="viewer" className="flex items-center gap-2">
|
<SelectItem value="viewer" className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LuUser className="size-4 text-primary" />
|
<LuUser className="size-4 text-primary" />
|
||||||
<span>Viewer</span>
|
<span><Trans>role.viewer</Trans></span>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -99,7 +89,7 @@ export default function RoleChangeDialog({
|
|||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Cancel
|
<Trans>button.cancel</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
@ -108,7 +98,7 @@ export default function RoleChangeDialog({
|
|||||||
onClick={() => onSave(selectedRole)}
|
onClick={() => onSave(selectedRole)}
|
||||||
disabled={selectedRole === currentRole}
|
disabled={selectedRole === currentRole}
|
||||||
>
|
>
|
||||||
Save
|
<Trans>button.save</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,7 +43,9 @@ export default function SaveExportOverlay({
|
|||||||
onClick={onPreview}
|
onClick={onPreview}
|
||||||
>
|
>
|
||||||
<LuVideo />
|
<LuVideo />
|
||||||
<Trans ns="components/dialog">export.fromTimeline.previewExport</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
export.fromTimeline.previewExport
|
||||||
|
</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
|
|||||||
@ -79,10 +79,10 @@ export default function SetPasswordDialog({
|
|||||||
|
|
||||||
const getStrengthLabel = () => {
|
const getStrengthLabel = () => {
|
||||||
if (!password) return "";
|
if (!password) return "";
|
||||||
if (passwordStrength <= 1) return "Weak";
|
if (passwordStrength <= 1) return t("users.dialog.form.password.strength.weak", { ns: "views/settings" });
|
||||||
if (passwordStrength === 2) return "Medium";
|
if (passwordStrength === 2) return t("users.dialog.form.password.strength.medium", { ns: "views/settings" });
|
||||||
if (passwordStrength === 3) return "Strong";
|
if (passwordStrength === 3) return t("users.dialog.form.password.strength.strong", { ns: "views/settings" });
|
||||||
return "Very Strong";
|
return t("users.dialog.form.password.strength.veryStrong", { ns: "views/settings" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStrengthColor = () => {
|
const getStrengthColor = () => {
|
||||||
@ -98,7 +98,14 @@ export default function SetPasswordDialog({
|
|||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader className="space-y-2">
|
<DialogHeader className="space-y-2">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{username ? t("users.dialog.passwordSetting.updatePassword", {username, ns: "views/settings"}) : t("users.dialog.passwordSetting.setPassword", {ns: "views/settings"})}
|
{username
|
||||||
|
? t("users.dialog.passwordSetting.updatePassword", {
|
||||||
|
username,
|
||||||
|
ns: "views/settings",
|
||||||
|
})
|
||||||
|
: t("users.dialog.passwordSetting.setPassword", {
|
||||||
|
ns: "views/settings",
|
||||||
|
})}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans>
|
<Trans ns="views/settings">users.dialog.passwordSetting.desc</Trans>
|
||||||
@ -107,7 +114,9 @@ export default function SetPasswordDialog({
|
|||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password"><Trans ns="views/settings">users.dialog.form.newPassword</Trans></Label>
|
<Label htmlFor="password">
|
||||||
|
<Trans ns="views/settings">users.dialog.form.newPassword</Trans>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
className="h-10"
|
className="h-10"
|
||||||
@ -117,7 +126,9 @@ export default function SetPasswordDialog({
|
|||||||
setPassword(event.target.value);
|
setPassword(event.target.value);
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
placeholder={t("users.dialog.form.newPassword.placeholder", {ns: "views/settings"})}
|
placeholder={t("users.dialog.form.newPassword.placeholder", {
|
||||||
|
ns: "views/settings",
|
||||||
|
})}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -131,7 +142,7 @@ export default function SetPasswordDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Password strength:{" "}
|
<Trans ns="views/settings">users.dialog.form.password.strength</Trans>
|
||||||
<span className="font-medium">{getStrengthLabel()}</span>
|
<span className="font-medium">{getStrengthLabel()}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -139,7 +150,11 @@ export default function SetPasswordDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="confirm-password"><Trans ns="views/settings">users.dialog.form.password.confirm</Trans></Label>
|
<Label htmlFor="confirm-password">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.confirm
|
||||||
|
</Trans>
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
className="h-10"
|
className="h-10"
|
||||||
@ -149,7 +164,7 @@ export default function SetPasswordDialog({
|
|||||||
setConfirmPassword(event.target.value);
|
setConfirmPassword(event.target.value);
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
placeholder="Confirm new password"
|
placeholder={t("users.dialog.form.newPassword.confirm.placeholder", { ns: "views/settings"})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Password match indicator */}
|
{/* Password match indicator */}
|
||||||
@ -158,12 +173,20 @@ export default function SetPasswordDialog({
|
|||||||
{password === confirmPassword ? (
|
{password === confirmPassword ? (
|
||||||
<>
|
<>
|
||||||
<LuCheck className="size-3.5 text-green-500" />
|
<LuCheck className="size-3.5 text-green-500" />
|
||||||
<span className="text-green-600"><Trans ns="views/settings">users.dialog.form.password.match</Trans></span>
|
<span className="text-green-600">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.match
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LuX className="size-3.5 text-red-500" />
|
<LuX className="size-3.5 text-red-500" />
|
||||||
<span className="text-red-600"><Trans ns="views/settings">users.dialog.form.password.notMatch</Trans></span>
|
<span className="text-red-600">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.dialog.form.password.notMatch
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -374,9 +374,12 @@ function ObjectDetailsTab({
|
|||||||
.post(`events/${search.id}/description`, { description: desc })
|
.post(`events/${search.id}/description`, { description: desc })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
toast.success(t("details.tips.descriptionSaved", {ns: "views/explore"}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("details.tips.descriptionSaved", { ns: "views/explore" }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
mutate(
|
mutate(
|
||||||
(key) =>
|
(key) =>
|
||||||
@ -407,9 +410,15 @@ function ObjectDetailsTab({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("details.tips.saveDescriptionFailed", {ns: "views/explore", errorMessage}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("details.tips.saveDescriptionFailed", {
|
||||||
});
|
ns: "views/explore",
|
||||||
|
errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
setDesc(search.data.description);
|
setDesc(search.data.description);
|
||||||
});
|
});
|
||||||
}, [desc, search, mutate]);
|
}, [desc, search, mutate]);
|
||||||
@ -580,7 +589,9 @@ function ObjectDetailsTab({
|
|||||||
{averageEstimatedSpeed && (
|
{averageEstimatedSpeed && (
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
{averageEstimatedSpeed}{" "}
|
{averageEstimatedSpeed}{" "}
|
||||||
{config?.ui.unit_system == "imperial" ? t("unit.speed.mph") : t("unit.speed.kph")}{" "}
|
{config?.ui.unit_system == "imperial"
|
||||||
|
? t("unit.speed.mph")
|
||||||
|
: t("unit.speed.kph")}{" "}
|
||||||
{velocityAngle != undefined && (
|
{velocityAngle != undefined && (
|
||||||
<span className="text-primary/40">
|
<span className="text-primary/40">
|
||||||
<FaArrowRight
|
<FaArrowRight
|
||||||
@ -668,7 +679,9 @@ function ObjectDetailsTab({
|
|||||||
<div className="text-sm text-primary/40"></div>
|
<div className="text-sm text-primary/40"></div>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-64"
|
className="h-64"
|
||||||
placeholder={t("details.description.placeholder", {ns: "views/explore"})}
|
placeholder={t("details.description.placeholder", {
|
||||||
|
ns: "views/explore",
|
||||||
|
})}
|
||||||
value={desc}
|
value={desc}
|
||||||
onChange={(e) => setDesc(e.target.value)}
|
onChange={(e) => setDesc(e.target.value)}
|
||||||
onFocus={handleDescriptionFocus}
|
onFocus={handleDescriptionFocus}
|
||||||
@ -734,13 +747,16 @@ function ObjectDetailsTab({
|
|||||||
<TextEntryDialog
|
<TextEntryDialog
|
||||||
open={isSubLabelDialogOpen}
|
open={isSubLabelDialogOpen}
|
||||||
setOpen={setIsSubLabelDialogOpen}
|
setOpen={setIsSubLabelDialogOpen}
|
||||||
title={t("details.editSubLable", {ns: "views/explore"})}
|
title={t("details.editSubLable", { ns: "views/explore" })}
|
||||||
description={
|
description={
|
||||||
search.label
|
search.label
|
||||||
? t("details.editSubLable.desc", {
|
? t("details.editSubLable.desc", {
|
||||||
label: t(search.label, { ns: "objects" }), ns: "views/explore",
|
label: t(search.label, { ns: "objects" }),
|
||||||
|
ns: "views/explore",
|
||||||
|
})
|
||||||
|
: t("details.editSubLable.desc.noLabel", {
|
||||||
|
ns: "views/explore",
|
||||||
})
|
})
|
||||||
: t("details.editSubLable.desc.noLabel", { ns: "views/explore" })
|
|
||||||
}
|
}
|
||||||
onSave={handleSubLabelSave}
|
onSave={handleSubLabelSave}
|
||||||
defaultValue={search?.sub_label || ""}
|
defaultValue={search?.sub_label || ""}
|
||||||
|
|||||||
@ -108,7 +108,10 @@ export default function RestartDialog({
|
|||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription className="text-center">
|
<SheetDescription className="text-center">
|
||||||
<div>
|
<div>
|
||||||
{t("restart.restarting.content", { countdown, ns: "components/dialog" })}
|
{t("restart.restarting.content", {
|
||||||
|
countdown,
|
||||||
|
ns: "components/dialog",
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|||||||
@ -753,7 +753,9 @@ 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"
|
||||||
>
|
>
|
||||||
<Trans ns="components/filter">features.submittedToFrigatePlus.label</Trans>
|
<Trans ns="components/filter">
|
||||||
|
features.submittedToFrigatePlus.label
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
|
|||||||
@ -325,7 +325,10 @@ 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">
|
||||||
<Trans ns="components/player" value={{ camera: camera.replaceAll("_", " ") }}>
|
<Trans
|
||||||
|
ns="components/player"
|
||||||
|
value={{ camera: camera.replaceAll("_", " ") }}
|
||||||
|
>
|
||||||
noPreviewFoundFor
|
noPreviewFoundFor
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
@ -547,7 +550,10 @@ 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">
|
||||||
<Trans ns="components/player" values={{ cameraName: camera.replaceAll("_", " ") }}>
|
<Trans
|
||||||
|
ns="components/player"
|
||||||
|
values={{ cameraName: camera.replaceAll("_", " ") }}
|
||||||
|
>
|
||||||
noPreviewFoundFor
|
noPreviewFoundFor
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -168,7 +168,10 @@ export function CameraStreamingDialog({
|
|||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader className="mb-4">
|
<DialogHeader className="mb-4">
|
||||||
<DialogTitle className="capitalize">
|
<DialogTitle className="capitalize">
|
||||||
<Trans ns="components/camera" values={{ cameraName: camera.replaceAll("_", " ") }}>
|
<Trans
|
||||||
|
ns="components/camera"
|
||||||
|
values={{ cameraName: camera.replaceAll("_", " ") }}
|
||||||
|
>
|
||||||
group.camera.setting.title
|
group.camera.setting.title
|
||||||
</Trans>
|
</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
@ -183,17 +186,23 @@ export function CameraStreamingDialog({
|
|||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
<LuX className="size-4 text-danger" />
|
<LuX className="size-4 text-danger" />
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.disabled
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div className="cursor-pointer p-0">
|
<div className="cursor-pointer p-0">
|
||||||
<LuInfo className="size-4" />
|
<LuInfo className="size-4" />
|
||||||
<span className="sr-only"><Trans>button.info</Trans></span>
|
<span className="sr-only">
|
||||||
|
<Trans>button.info</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80 text-xs">
|
<PopoverContent className="w-80 text-xs">
|
||||||
<Trans ns="components/dialog">streaming.restreaming.desc</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.desc
|
||||||
|
</Trans>
|
||||||
<div className="mt-2 flex items-center text-primary">
|
<div className="mt-2 flex items-center text-primary">
|
||||||
<Link
|
<Link
|
||||||
to="https://docs.frigate.video/configuration/live"
|
to="https://docs.frigate.video/configuration/live"
|
||||||
@ -286,7 +295,9 @@ export function CameraStreamingDialog({
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<Label htmlFor="streaming-method" className="text-right">
|
<Label htmlFor="streaming-method" className="text-right">
|
||||||
<Trans ns="components/camera">group.camera.setting.streamMethod</Trans>
|
<Trans ns="components/camera">
|
||||||
|
group.camera.setting.streamMethod
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={streamType}
|
value={streamType}
|
||||||
@ -357,7 +368,9 @@ export function CameraStreamingDialog({
|
|||||||
htmlFor="compatibility"
|
htmlFor="compatibility"
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
>
|
>
|
||||||
<Trans ns="components/camera">group.camera.setting.compatibilityMode</Trans>
|
<Trans ns="components/camera">
|
||||||
|
group.camera.setting.compatibilityMode
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 leading-none">
|
<div className="flex flex-col gap-2 leading-none">
|
||||||
|
|||||||
@ -107,7 +107,9 @@ export default function MotionMaskEditPane({
|
|||||||
polygon: z.object({ name: z.string(), isFinished: z.boolean() }),
|
polygon: z.object({ name: z.string(), isFinished: z.boolean() }),
|
||||||
})
|
})
|
||||||
.refine(() => polygon?.isFinished === true, {
|
.refine(() => polygon?.isFinished === true, {
|
||||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {ns: "views/settings"}),
|
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
path: ["polygon.isFinished"],
|
path: ["polygon.isFinished"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,19 +168,13 @@ export default function MotionMaskEditPane({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(
|
toast.success(
|
||||||
polygon.name
|
polygon.name
|
||||||
? t(
|
? t("masksAndZones.motionMasks.toast.success", {
|
||||||
"masksAndZones.motionMasks.toast.success",
|
polygonName: polygon.name,
|
||||||
{
|
ns: "views/settings",
|
||||||
polygonName: polygon.name,
|
})
|
||||||
ns: "views/settings"
|
: t("masksAndZones.motionMasks.toast.success.noName", {
|
||||||
},
|
ns: "views/settings",
|
||||||
)
|
}),
|
||||||
: t(
|
|
||||||
"masksAndZones.motionMasks.toast.success.noName",
|
|
||||||
{
|
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
},
|
},
|
||||||
@ -224,12 +220,9 @@ export default function MotionMaskEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t(
|
document.title = t("masksAndZones.motionMasks.documentTitle", {
|
||||||
"masksAndZones.motionMasks.documentTitle",
|
ns: "views/settings",
|
||||||
{
|
});
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
@ -241,14 +234,12 @@ export default function MotionMaskEditPane({
|
|||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
{polygon.name.length
|
{polygon.name.length
|
||||||
? t("masksAndZones.motionMasks.edit", {ns: "views/settings"})
|
? t("masksAndZones.motionMasks.edit", { ns: "views/settings" })
|
||||||
: t("masksAndZones.motionMasks.add", {ns: "views/settings"})}
|
: t("masksAndZones.motionMasks.add", { ns: "views/settings" })}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
<div className="my-3 space-y-3 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">masksAndZones.motionMasks.context</Trans>
|
||||||
masksAndZones.motionMasks.context
|
|
||||||
</Trans>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center text-primary">
|
<div className="flex items-center text-primary">
|
||||||
@ -271,7 +262,7 @@ export default function MotionMaskEditPane({
|
|||||||
<div className="my-1 inline-flex">
|
<div className="my-1 inline-flex">
|
||||||
{t("masksAndZones.motionMasks.point", {
|
{t("masksAndZones.motionMasks.point", {
|
||||||
count: polygons[activePolygonIndex].points.length,
|
count: polygons[activePolygonIndex].points.length,
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
{polygons[activePolygonIndex].isFinished && (
|
{polygons[activePolygonIndex].isFinished && (
|
||||||
<FaCheckCircle className="ml-2 size-5" />
|
<FaCheckCircle className="ml-2 size-5" />
|
||||||
@ -297,13 +288,10 @@ export default function MotionMaskEditPane({
|
|||||||
{polygonArea && polygonArea >= 0.35 && (
|
{polygonArea && polygonArea >= 0.35 && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 text-sm text-danger">
|
<div className="mb-3 text-sm text-danger">
|
||||||
{t(
|
{t("masksAndZones.motionMasks.polygonAreaTooLarge", {
|
||||||
"masksAndZones.motionMasks.polygonAreaTooLarge",
|
polygonArea: Math.round(polygonArea * 100),
|
||||||
{
|
ns: "views/settings",
|
||||||
polygonArea: Math.round(polygonArea * 100),
|
})}
|
||||||
ns: "views/settings"
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 text-sm text-primary">
|
<div className="mb-3 text-sm text-primary">
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">
|
||||||
|
|||||||
@ -109,7 +109,9 @@ export default function ObjectMaskEditPane({
|
|||||||
polygon: z.object({ isFinished: z.boolean(), name: z.string() }),
|
polygon: z.object({ isFinished: z.boolean(), name: z.string() }),
|
||||||
})
|
})
|
||||||
.refine(() => polygon?.isFinished === true, {
|
.refine(() => polygon?.isFinished === true, {
|
||||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {ns: "views/settings"}),
|
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
path: ["polygon.isFinished"],
|
path: ["polygon.isFinished"],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -198,19 +200,13 @@ export default function ObjectMaskEditPane({
|
|||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(
|
toast.success(
|
||||||
polygon.name
|
polygon.name
|
||||||
? t(
|
? t("masksAndZones.objectMasks.toast.success", {
|
||||||
"masksAndZones.objectMasks.toast.success",
|
polygonName: polygon.name,
|
||||||
{
|
ns: "views/settings",
|
||||||
polygonName: polygon.name,
|
})
|
||||||
ns: "views/settings"
|
: t("masksAndZones.objectMasks.toast.success.noName", {
|
||||||
},
|
ns: "views/settings",
|
||||||
)
|
}),
|
||||||
: t(
|
|
||||||
"masksAndZones.objectMasks.toast.success.noName",
|
|
||||||
{
|
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
},
|
},
|
||||||
@ -232,11 +228,14 @@ export default function ObjectMaskEditPane({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("toast.save.error", {
|
toast.error(
|
||||||
errorMessage
|
t("toast.save.error", {
|
||||||
}), {
|
errorMessage,
|
||||||
position: "top-center",
|
}),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -265,12 +264,9 @@ export default function ObjectMaskEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t(
|
document.title = t("masksAndZones.objectMasks.documentTitle", {
|
||||||
"masksAndZones.objectMasks.documentTitle",
|
ns: "views/settings",
|
||||||
{
|
});
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
@ -283,17 +279,15 @@ export default function ObjectMaskEditPane({
|
|||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
{polygon.name.length
|
{polygon.name.length
|
||||||
? t("masksAndZones.objectMasks.edit", {
|
? t("masksAndZones.objectMasks.edit", {
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})
|
})
|
||||||
: t("masksAndZones.objectMasks.add", {
|
: t("masksAndZones.objectMasks.add", {
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="my-2 text-sm text-muted-foreground">
|
<div className="my-2 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">masksAndZones.objectMasks.context</Trans>
|
||||||
masksAndZones.objectMasks.context
|
|
||||||
</Trans>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-3 bg-secondary" />
|
<Separator className="my-3 bg-secondary" />
|
||||||
@ -302,7 +296,7 @@ export default function ObjectMaskEditPane({
|
|||||||
<div className="my-1 inline-flex">
|
<div className="my-1 inline-flex">
|
||||||
{t("masksAndZones.objectMasks.point", {
|
{t("masksAndZones.objectMasks.point", {
|
||||||
count: polygons[activePolygonIndex].points.length,
|
count: polygons[activePolygonIndex].points.length,
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
{polygons[activePolygonIndex].isFinished && (
|
{polygons[activePolygonIndex].isFinished && (
|
||||||
<FaCheckCircle className="ml-2 size-5" />
|
<FaCheckCircle className="ml-2 size-5" />
|
||||||
@ -474,7 +468,7 @@ export function ZoneObjectSelector({ camera }: ZoneObjectSelectorProps) {
|
|||||||
<SelectSeparator className="bg-secondary" />
|
<SelectSeparator className="bg-secondary" />
|
||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<SelectItem key={item} value={item}>
|
<SelectItem key={item} value={item}>
|
||||||
{t(item, {ns: "objects"})}
|
{t(item, { ns: "objects" })}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@ -63,7 +63,9 @@ export default function ExploreSettings({
|
|||||||
<Trans ns="components/filter">explore.settings.defaultView</Trans>
|
<Trans ns="components/filter">explore.settings.defaultView</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
<Trans ns="components/filter">explore.settings.defaultView.desc</Trans>
|
<Trans ns="components/filter">
|
||||||
|
explore.settings.defaultView.desc
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@ -72,8 +74,12 @@ export default function ExploreSettings({
|
|||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
{defaultView == "summary"
|
{defaultView == "summary"
|
||||||
? t("explore.settings.defaultView.summary", {ns: "components/filter"})
|
? t("explore.settings.defaultView.summary", {
|
||||||
: t("explore.settings.defaultView.unfilteredGrid", {ns: "components/filter"})}
|
ns: "components/filter",
|
||||||
|
})
|
||||||
|
: t("explore.settings.defaultView.unfilteredGrid", {
|
||||||
|
ns: "components/filter",
|
||||||
|
})}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
@ -84,8 +90,12 @@ export default function ExploreSettings({
|
|||||||
value={value}
|
value={value}
|
||||||
>
|
>
|
||||||
{value == "summary"
|
{value == "summary"
|
||||||
? t("explore.settings.defaultView.summary", {ns: "components/filter"})
|
? t("explore.settings.defaultView.summary", {
|
||||||
: t("explore.settings.defaultView.unfilteredGrid", {ns: "components/filter"})}
|
ns: "components/filter",
|
||||||
|
})
|
||||||
|
: t("explore.settings.defaultView.unfilteredGrid", {
|
||||||
|
ns: "components/filter",
|
||||||
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
@ -98,10 +108,14 @@ export default function ExploreSettings({
|
|||||||
<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">
|
<div className="text-md">
|
||||||
<Trans ns="components/filter">explore.settings.gridColumns</Trans>
|
<Trans ns="components/filter">
|
||||||
|
explore.settings.gridColumns
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
<Trans ns="components/filter">explore.settings.gridColumns.desc</Trans>
|
<Trans ns="components/filter">
|
||||||
|
explore.settings.gridColumns.desc
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
@ -166,7 +180,9 @@ export function SearchTypeContent({
|
|||||||
<Trans ns="components/filter">explore.settings.searchSource</Trans>
|
<Trans ns="components/filter">explore.settings.searchSource</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
<div className="space-y-1 text-xs text-muted-foreground">
|
||||||
<Trans ns="components/filter">explore.settings.searchSource.desc</Trans>
|
<Trans ns="components/filter">
|
||||||
|
explore.settings.searchSource.desc
|
||||||
|
</Trans>
|
||||||
</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">
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export default function ZoneEditPane({
|
|||||||
.min(2, {
|
.min(2, {
|
||||||
message: t(
|
message: t(
|
||||||
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
|
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
|
||||||
{ ns: "views/settings"}
|
{ ns: "views/settings" },
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
.transform((val: string) => val.trim().replace(/\s+/g, "_"))
|
||||||
@ -117,7 +117,7 @@ export default function ZoneEditPane({
|
|||||||
{
|
{
|
||||||
message: t(
|
message: t(
|
||||||
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
|
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
|
||||||
{ ns: "views/settings"}
|
{ ns: "views/settings" },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -131,9 +131,9 @@ export default function ZoneEditPane({
|
|||||||
return !otherPolygonNames.includes(value);
|
return !otherPolygonNames.includes(value);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t("masksAndZones.form.zoneName.error.alreadyExists",
|
message: t("masksAndZones.form.zoneName.error.alreadyExists", {
|
||||||
{ ns: "views/settings"}
|
ns: "views/settings",
|
||||||
),
|
}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@ -141,22 +141,23 @@ export default function ZoneEditPane({
|
|||||||
return !value.includes(".");
|
return !value.includes(".");
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t("masksAndZones.form.zoneName.error.mustNotContainPeriod",
|
message: t(
|
||||||
{ ns: "views/settings"}
|
"masksAndZones.form.zoneName.error.mustNotContainPeriod",
|
||||||
|
{ ns: "views/settings" },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
|
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
|
||||||
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter",
|
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter", {
|
||||||
{ ns: "views/settings"}
|
ns: "views/settings",
|
||||||
),
|
}),
|
||||||
}),
|
}),
|
||||||
inertia: z.coerce
|
inertia: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(1, {
|
.min(1, {
|
||||||
message: t("masksAndZones.form.inertia.error.mustBeAboveZero",
|
message: t("masksAndZones.form.inertia.error.mustBeAboveZero", {
|
||||||
{ ns: "views/settings"}
|
ns: "views/settings",
|
||||||
),
|
}),
|
||||||
})
|
})
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
loitering_time: z.coerce
|
loitering_time: z.coerce
|
||||||
@ -164,13 +165,15 @@ export default function ZoneEditPane({
|
|||||||
.min(0, {
|
.min(0, {
|
||||||
message: t(
|
message: t(
|
||||||
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
|
"masksAndZones.form.loiteringTime.error.mustBeGreaterOrEqualZero",
|
||||||
{ ns: "views/settings"}
|
{ ns: "views/settings" },
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
|
isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
|
||||||
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", { ns: "views/settings" }),
|
message: t("masksAndZones.polygonDrawing.error.mustBeFinished", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
objects: z.array(z.string()).optional(),
|
objects: z.array(z.string()).optional(),
|
||||||
review_alerts: z.boolean().default(false).optional(),
|
review_alerts: z.boolean().default(false).optional(),
|
||||||
@ -179,28 +182,36 @@ export default function ZoneEditPane({
|
|||||||
lineA: z.coerce
|
lineA: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}),
|
message: t("masksAndZones.form.distance.error", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineB: z.coerce
|
lineB: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}),
|
message: t("masksAndZones.form.distance.error", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineC: z.coerce
|
lineC: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}),
|
message: t("masksAndZones.form.distance.error", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
lineD: z.coerce
|
lineD: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(0.1, {
|
.min(0.1, {
|
||||||
message: t("masksAndZones.form.distance.error", { ns: "views/settings"}),
|
message: t("masksAndZones.form.distance.error", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("")),
|
.or(z.literal("")),
|
||||||
@ -220,7 +231,9 @@ export default function ZoneEditPane({
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t("masksAndZones.form.distance.error.mustBeFilled", { ns: "views/settings"}),
|
message: t("masksAndZones.form.distance.error.mustBeFilled", {
|
||||||
|
ns: "views/settings",
|
||||||
|
}),
|
||||||
path: ["speedEstimation"],
|
path: ["speedEstimation"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -237,8 +250,8 @@ export default function ZoneEditPane({
|
|||||||
message: t(
|
message: t(
|
||||||
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
|
"masksAndZones.zones.speedThreshold.toast.error.loiteringTimeError",
|
||||||
{
|
{
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
path: ["loitering_time"],
|
path: ["loitering_time"],
|
||||||
},
|
},
|
||||||
@ -278,12 +291,9 @@ export default function ZoneEditPane({
|
|||||||
polygon.points.length !== 4
|
polygon.points.length !== 4
|
||||||
) {
|
) {
|
||||||
toast.error(
|
toast.error(
|
||||||
t(
|
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError", {
|
||||||
"masksAndZones.zones.speedThreshold.toast.error.pointLengthError",
|
ns: "views/settings",
|
||||||
{
|
}),
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
form.setValue("speedEstimation", false);
|
form.setValue("speedEstimation", false);
|
||||||
}
|
}
|
||||||
@ -452,11 +462,14 @@ export default function ZoneEditPane({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("toast.save.error", {
|
toast.error(
|
||||||
errorMessage,
|
t("toast.save.error", {
|
||||||
}), {
|
errorMessage,
|
||||||
position: "top-center",
|
}),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -490,12 +503,9 @@ export default function ZoneEditPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = t(
|
document.title = t("masksAndZones.zones.documentTitle", {
|
||||||
"masksAndZones.zones.documentTitle",
|
ns: "views/settings",
|
||||||
{
|
});
|
||||||
ns: "views/settings"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!polygon) {
|
if (!polygon) {
|
||||||
@ -507,12 +517,12 @@ export default function ZoneEditPane({
|
|||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
{polygon.name.length
|
{polygon.name.length
|
||||||
? t("masksAndZones.zones.edit",{
|
? t("masksAndZones.zones.edit", {
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})
|
})
|
||||||
: t("masksAndZones.zones.add",{
|
: t("masksAndZones.zones.add", {
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="my-2 text-sm text-muted-foreground">
|
<div className="my-2 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
@ -525,7 +535,7 @@ export default function ZoneEditPane({
|
|||||||
<div className="my-1 inline-flex">
|
<div className="my-1 inline-flex">
|
||||||
{t("masksAndZones.zones.point", {
|
{t("masksAndZones.zones.point", {
|
||||||
count: polygons[activePolygonIndex].points.length,
|
count: polygons[activePolygonIndex].points.length,
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{polygons[activePolygonIndex].isFinished && (
|
{polygons[activePolygonIndex].isFinished && (
|
||||||
@ -542,9 +552,7 @@ export default function ZoneEditPane({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-3 text-sm text-muted-foreground">
|
<div className="mb-3 text-sm text-muted-foreground">
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">masksAndZones.zones.clickDrawPolygon</Trans>
|
||||||
masksAndZones.zones.clickDrawPolygon
|
|
||||||
</Trans>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-3 bg-secondary" />
|
<Separator className="my-3 bg-secondary" />
|
||||||
@ -565,8 +573,8 @@ export default function ZoneEditPane({
|
|||||||
placeholder={t(
|
placeholder={t(
|
||||||
"masksAndZones.zones.name.inputPlaceHolder",
|
"masksAndZones.zones.name.inputPlaceHolder",
|
||||||
{
|
{
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
}
|
},
|
||||||
)}
|
)}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -587,9 +595,7 @@ export default function ZoneEditPane({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">masksAndZones.zones.inertia</Trans>
|
||||||
masksAndZones.zones.inertia
|
|
||||||
</Trans>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@ -693,8 +699,8 @@ export default function ZoneEditPane({
|
|||||||
t(
|
t(
|
||||||
"masksAndZones.zones.speedEstimation.pointLengthError",
|
"masksAndZones.zones.speedEstimation.pointLengthError",
|
||||||
{
|
{
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -707,8 +713,8 @@ export default function ZoneEditPane({
|
|||||||
t(
|
t(
|
||||||
"masksAndZones.zones.speedEstimation.loiteringTimeError",
|
"masksAndZones.zones.speedEstimation.loiteringTimeError",
|
||||||
{
|
{
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -996,7 +1002,7 @@ export function ZoneObjectSelector({
|
|||||||
className="w-full cursor-pointer capitalize text-primary"
|
className="w-full cursor-pointer capitalize text-primary"
|
||||||
htmlFor={item}
|
htmlFor={item}
|
||||||
>
|
>
|
||||||
{t(item, {ns: "objects"})}
|
{t(item, { ns: "objects" })}
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
key={item}
|
key={item}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function useStats(stats: FrigateStats | undefined) {
|
|||||||
text: t("stats.ffmpegHighCpuUsage", {
|
text: t("stats.ffmpegHighCpuUsage", {
|
||||||
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
||||||
ffmpegAvg,
|
ffmpegAvg,
|
||||||
ns: "views/system"
|
ns: "views/system",
|
||||||
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
relevantLink: "/system#cameras",
|
relevantLink: "/system#cameras",
|
||||||
@ -88,7 +88,7 @@ export default function useStats(stats: FrigateStats | undefined) {
|
|||||||
text: t("stats.detectHighCpuUsage", {
|
text: t("stats.detectHighCpuUsage", {
|
||||||
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
|
||||||
detectAvg,
|
detectAvg,
|
||||||
ns: "views/system"
|
ns: "views/system",
|
||||||
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
}), //`${capitalizeFirstLetter(name.replaceAll("_", " "))} has high detect CPU usage (${detectAvg}%)`,
|
||||||
color: "text-danger",
|
color: "text-danger",
|
||||||
relevantLink: "/system#cameras",
|
relevantLink: "/system#cameras",
|
||||||
|
|||||||
@ -78,9 +78,9 @@ export default function Events() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (recording) {
|
if (recording) {
|
||||||
document.title = t("recordings.documentTitle", {ns: "views/events"});
|
document.title = t("recordings.documentTitle", { ns: "views/events" });
|
||||||
} else {
|
} else {
|
||||||
document.title = t("documentTitle", {ns: "views/events"});
|
document.title = t("documentTitle", { ns: "views/events" });
|
||||||
}
|
}
|
||||||
}, [recording, severity]);
|
}, [recording, severity]);
|
||||||
|
|
||||||
|
|||||||
@ -121,7 +121,10 @@ function Exports() {
|
|||||||
<Trans ns="views/exports">deleteExport</Trans>
|
<Trans ns="views/exports">deleteExport</Trans>
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
<Trans ns="views/exports" values={{ exportName: deleteClip?.exportName }}>
|
<Trans
|
||||||
|
ns="views/exports"
|
||||||
|
values={{ exportName: deleteClip?.exportName }}
|
||||||
|
>
|
||||||
deleteExport.desc
|
deleteExport.desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|||||||
@ -67,15 +67,15 @@ function Live() {
|
|||||||
.map((text) => text[0].toUpperCase() + text.substring(1));
|
.map((text) => text[0].toUpperCase() + text.substring(1));
|
||||||
document.title = t("documentTitle.withCamera", {
|
document.title = t("documentTitle.withCamera", {
|
||||||
camera: capitalized.join(" "),
|
camera: capitalized.join(" "),
|
||||||
ns: "views/live"
|
ns: "views/live",
|
||||||
});
|
});
|
||||||
} else if (cameraGroup && cameraGroup != "default") {
|
} else if (cameraGroup && cameraGroup != "default") {
|
||||||
document.title = t("documentTitle.withCamera", {
|
document.title = t("documentTitle.withCamera", {
|
||||||
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
camera: `${cameraGroup[0].toUpperCase()}${cameraGroup.substring(1)}`,
|
||||||
ns: "views/live"
|
ns: "views/live",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
document.title = t("documentTitle", {ns: "views/live"});
|
document.title = t("documentTitle", { ns: "views/live" });
|
||||||
}
|
}
|
||||||
}, [cameraGroup, selectedCameraName]);
|
}, [cameraGroup, selectedCameraName]);
|
||||||
|
|
||||||
|
|||||||
@ -94,7 +94,9 @@ function System() {
|
|||||||
{item == "storage" && <LuHardDrive className="size-4" />}
|
{item == "storage" && <LuHardDrive className="size-4" />}
|
||||||
{item == "cameras" && <FaVideo className="size-4" />}
|
{item == "cameras" && <FaVideo className="size-4" />}
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div className="capitalize">{t(item+".title", {ns:"views/system"})}</div>
|
<div className="capitalize">
|
||||||
|
{t(item + ".title", { ns: "views/system" })}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
))}
|
))}
|
||||||
@ -110,7 +112,9 @@ function System() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-end gap-2">
|
<div className="mt-2 flex items-end gap-2">
|
||||||
<div className="h-full content-center font-medium"><Trans ns="views/system">title</Trans></div>
|
<div className="h-full content-center font-medium">
|
||||||
|
<Trans ns="views/system">title</Trans>
|
||||||
|
</div>
|
||||||
{statsSnapshot && (
|
{statsSnapshot && (
|
||||||
<div className="h-full content-center text-sm text-muted-foreground">
|
<div className="h-full content-center text-sm text-muted-foreground">
|
||||||
{statsSnapshot.service.version}
|
{statsSnapshot.service.version}
|
||||||
|
|||||||
@ -13,23 +13,23 @@ i18n
|
|||||||
},
|
},
|
||||||
|
|
||||||
ns: [
|
ns: [
|
||||||
'common',
|
"common",
|
||||||
'objects',
|
"objects",
|
||||||
'audio',
|
"audio",
|
||||||
'components/camera',
|
"components/camera",
|
||||||
'components/dialog',
|
"components/dialog",
|
||||||
'components/filter',
|
"components/filter",
|
||||||
'components/icons',
|
"components/icons",
|
||||||
'components/player',
|
"components/player",
|
||||||
'views/events',
|
"views/events",
|
||||||
'views/explore',
|
"views/explore",
|
||||||
'views/live',
|
"views/live",
|
||||||
'views/settings',
|
"views/settings",
|
||||||
'views/system',
|
"views/system",
|
||||||
'views/exports',
|
"views/exports",
|
||||||
'views/explore'
|
"views/explore",
|
||||||
],
|
],
|
||||||
defaultNS: 'common',
|
defaultNS: "common",
|
||||||
|
|
||||||
react: {
|
react: {
|
||||||
transSupportBasicHtmlNodes: true,
|
transSupportBasicHtmlNodes: true,
|
||||||
@ -41,6 +41,11 @@ i18n
|
|||||||
"li",
|
"li",
|
||||||
"p",
|
"p",
|
||||||
"code",
|
"code",
|
||||||
|
"span",
|
||||||
|
"p",
|
||||||
|
"ul",
|
||||||
|
"li",
|
||||||
|
"ol",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
interpolation: {
|
interpolation: {
|
||||||
@ -52,10 +57,15 @@ i18n
|
|||||||
|
|
||||||
// Handle special cases for objects and audio
|
// Handle special cases for objects and audio
|
||||||
if (parts[0] === "object" || parts[0] === "audio") {
|
if (parts[0] === "object" || parts[0] === "audio") {
|
||||||
return parts[1]
|
return (
|
||||||
?.split("_")
|
parts[1]
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
?.split("_")
|
||||||
.join(" ") || key;
|
.map(
|
||||||
|
(word) =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
||||||
|
)
|
||||||
|
.join(" ") || key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For nested keys, try to make them more readable
|
// For nested keys, try to make them more readable
|
||||||
@ -63,14 +73,19 @@ i18n
|
|||||||
const lastPart = parts[parts.length - 1];
|
const lastPart = parts[parts.length - 1];
|
||||||
return lastPart
|
return lastPart
|
||||||
.split("_")
|
.split("_")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
.map(
|
||||||
|
(word) =>
|
||||||
|
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
||||||
|
)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single keys, just capitalize and format
|
// For single keys, just capitalize and format
|
||||||
return key
|
return key
|
||||||
.split("_")
|
.split("_")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
.map(
|
||||||
|
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
||||||
|
)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -199,9 +199,12 @@ export default function EventView({
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
toast.success(t("export.toast.success", { ns: "components/dialog"}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("export.toast.success", { ns: "components/dialog" }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -209,9 +212,15 @@ export default function EventView({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("export.toast.error", { ns: "components/dialog", message: errorMessage }), {
|
toast.error(
|
||||||
position: "top-center",
|
t("export.toast.error", {
|
||||||
});
|
ns: "components/dialog",
|
||||||
|
message: errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[reviewItems],
|
[reviewItems],
|
||||||
|
|||||||
@ -695,7 +695,9 @@ export default function DraggableGridLayout({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{fullscreen ? t("button.exitFullscreen") : t("button.fullscreen")}
|
{fullscreen
|
||||||
|
? t("button.exitFullscreen")
|
||||||
|
: t("button.fullscreen")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -493,7 +493,9 @@ export default function LiveCameraView({
|
|||||||
variant={fullscreen ? "overlay" : "primary"}
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={fullscreen ? FaCompress : FaExpand}
|
Icon={fullscreen ? FaCompress : FaExpand}
|
||||||
isActive={fullscreen}
|
isActive={fullscreen}
|
||||||
title={fullscreen ? t("button.close") : t("button.fullscreen")}
|
title={
|
||||||
|
fullscreen ? t("button.close") : t("button.fullscreen")
|
||||||
|
}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -764,7 +766,7 @@ function PtzControlPanel({
|
|||||||
{ptz?.features?.includes("pt") && (
|
{ptz?.features?.includes("pt") && (
|
||||||
<>
|
<>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.move.left.label", { ns: "views/live"})}
|
label={t("ptz.move.left.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_LEFT");
|
sendPtz("MOVE_LEFT");
|
||||||
@ -779,7 +781,7 @@ function PtzControlPanel({
|
|||||||
<FaAngleLeft />
|
<FaAngleLeft />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.move.up.label", { ns: "views/live"})}
|
label={t("ptz.move.up.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_UP");
|
sendPtz("MOVE_UP");
|
||||||
@ -794,7 +796,7 @@ function PtzControlPanel({
|
|||||||
<FaAngleUp />
|
<FaAngleUp />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.move.down.label", { ns: "views/live"})}
|
label={t("ptz.move.down.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_DOWN");
|
sendPtz("MOVE_DOWN");
|
||||||
@ -809,7 +811,7 @@ function PtzControlPanel({
|
|||||||
<FaAngleDown />
|
<FaAngleDown />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.move.right.label", { ns: "views/live"})}
|
label={t("ptz.move.right.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_RIGHT");
|
sendPtz("MOVE_RIGHT");
|
||||||
@ -828,7 +830,7 @@ function PtzControlPanel({
|
|||||||
{ptz?.features?.includes("zoom") && (
|
{ptz?.features?.includes("zoom") && (
|
||||||
<>
|
<>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.zoom.in.label", { ns: "views/live"})}
|
label={t("ptz.zoom.in.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("ZOOM_IN");
|
sendPtz("ZOOM_IN");
|
||||||
@ -843,7 +845,7 @@ function PtzControlPanel({
|
|||||||
<MdZoomIn />
|
<MdZoomIn />
|
||||||
</TooltipButton>
|
</TooltipButton>
|
||||||
<TooltipButton
|
<TooltipButton
|
||||||
label={t("ptz.zoom.out.label", { ns: "views/live"})}
|
label={t("ptz.zoom.out.label", { ns: "views/live" })}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("ZOOM_OUT");
|
sendPtz("ZOOM_OUT");
|
||||||
@ -1027,7 +1029,9 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
{!camera.record.enabled || camera.record.retain.days == 0 ? (
|
{!camera.record.enabled || camera.record.retain.days == 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="views/live">manualRecording.recordDisabledTips</Trans>
|
<Trans ns="views/live">
|
||||||
|
manualRecording.recordDisabledTips
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<OnDemandRetentionMessage camera={camera} />
|
<OnDemandRetentionMessage camera={camera} />
|
||||||
@ -1041,7 +1045,7 @@ function FrigateCameraFeatures({
|
|||||||
setActiveToastId(toastId);
|
setActiveToastId(toastId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("manualRecording.failedToStart", { ns: "views/live"}), {
|
toast.error(t("manualRecording.failedToStart", { ns: "views/live" }), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1058,12 +1062,12 @@ function FrigateCameraFeatures({
|
|||||||
});
|
});
|
||||||
recordingEventIdRef.current = null;
|
recordingEventIdRef.current = null;
|
||||||
setIsRecording(false);
|
setIsRecording(false);
|
||||||
toast.success(t("manualRecording.ended", { ns: "views/live"}), {
|
toast.success(t("manualRecording.ended", { ns: "views/live" }), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("manualRecording.failedToEnd", { ns: "views/live"}), {
|
toast.error(t("manualRecording.failedToEnd", { ns: "views/live" }), {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1170,9 +1174,9 @@ function FrigateCameraFeatures({
|
|||||||
variant={fullscreen ? "overlay" : "primary"}
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
|
Icon={isRecording ? TbRecordMail : TbRecordMailOff}
|
||||||
isActive={isRecording}
|
isActive={isRecording}
|
||||||
title={t(
|
title={t("manualRecording." + (isRecording ? "stop" : "start"), {
|
||||||
"manualRecording." + (isRecording ? "stop" : "start"), { ns: "views/live"})
|
ns: "views/live",
|
||||||
}
|
})}
|
||||||
onClick={handleEventButtonClick}
|
onClick={handleEventButtonClick}
|
||||||
disabled={!cameraEnabled}
|
disabled={!cameraEnabled}
|
||||||
/>
|
/>
|
||||||
@ -1199,7 +1203,9 @@ function FrigateCameraFeatures({
|
|||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
<LuX className="size-4 text-danger" />
|
<LuX className="size-4 text-danger" />
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.disabled
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -1209,7 +1215,9 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80 text-xs">
|
<PopoverContent className="w-80 text-xs">
|
||||||
<Trans ns="components/dialog">streaming.restreaming.desc</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.desc
|
||||||
|
</Trans>
|
||||||
<div className="mt-2 flex items-center text-primary">
|
<div className="mt-2 flex items-center text-primary">
|
||||||
<Link
|
<Link
|
||||||
to="https://docs.frigate.video/configuration/live"
|
to="https://docs.frigate.video/configuration/live"
|
||||||
@ -1512,7 +1520,9 @@ function FrigateCameraFeatures({
|
|||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
<LuX className="size-4 text-danger" />
|
<LuX className="size-4 text-danger" />
|
||||||
<div>
|
<div>
|
||||||
<Trans ns="components/dialog">streaming.restreaming.disabled</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.disabled
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@ -1522,7 +1532,9 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80 text-xs">
|
<PopoverContent className="w-80 text-xs">
|
||||||
<Trans ns="components/dialog">streaming.restreaming.desc</Trans>
|
<Trans ns="components/dialog">
|
||||||
|
streaming.restreaming.desc
|
||||||
|
</Trans>
|
||||||
<div className="mt-2 flex items-center text-primary">
|
<div className="mt-2 flex items-center text-primary">
|
||||||
<Link
|
<Link
|
||||||
to="https://docs.frigate.video/configuration/live"
|
to="https://docs.frigate.video/configuration/live"
|
||||||
|
|||||||
@ -563,7 +563,9 @@ export default function LiveDashboardView({
|
|||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{fullscreen ? t("button.exitFullscreen") : t("button.fullscreen")}
|
{fullscreen
|
||||||
|
? t("button.exitFullscreen")
|
||||||
|
: t("button.fullscreen")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -67,9 +67,15 @@ export default function AuthenticationView() {
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("users.toast.error.setPasswordFailed", {ns: "views/settings", errorMessage}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("users.toast.error.setPasswordFailed", {
|
||||||
});
|
ns: "views/settings",
|
||||||
|
errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -87,9 +93,12 @@ export default function AuthenticationView() {
|
|||||||
users?.push({ username: user, role: role });
|
users?.push({ username: user, role: role });
|
||||||
return users;
|
return users;
|
||||||
}, false);
|
}, false);
|
||||||
toast.success(t("users.toast.success.createUser", {ns: "views/settings", user}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("users.toast.success.createUser", { ns: "views/settings", user }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -97,9 +106,15 @@ export default function AuthenticationView() {
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("users.toast.error.createUserFailed", {ns: "views/settings", errorMessage}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("users.toast.error.createUserFailed", {
|
||||||
});
|
ns: "views/settings",
|
||||||
|
errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -113,9 +128,12 @@ export default function AuthenticationView() {
|
|||||||
(users) => users?.filter((u) => u.username !== user),
|
(users) => users?.filter((u) => u.username !== user),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
toast.success(t("users.toast.success.deleteUser", {ns: "views/settings", user}), {
|
toast.success(
|
||||||
position: "top-center",
|
t("users.toast.success.deleteUser", { ns: "views/settings", user }),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -123,9 +141,15 @@ export default function AuthenticationView() {
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("users.toast.error.deleteUserFailed", {ns: "views/settings", errorMessage}), {
|
toast.error(
|
||||||
position: "top-center",
|
t("users.toast.error.deleteUserFailed", {
|
||||||
});
|
ns: "views/settings",
|
||||||
|
errorMessage,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -188,7 +212,7 @@ export default function AuthenticationView() {
|
|||||||
onClick={() => setShowCreate(true)}
|
onClick={() => setShowCreate(true)}
|
||||||
>
|
>
|
||||||
<LuPlus className="size-4" />
|
<LuPlus className="size-4" />
|
||||||
Add User
|
<Trans ns="views/settings">users.addUser</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
@ -197,9 +221,13 @@ export default function AuthenticationView() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="sticky top-0 bg-muted/50">
|
<TableHeader className="sticky top-0 bg-muted/50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[250px]"><Trans ns="views/settings">users.table.username</Trans></TableHead>
|
<TableHead className="w-[250px]">
|
||||||
<TableHead>Role</TableHead>
|
<Trans ns="views/settings">users.table.username</Trans>
|
||||||
<TableHead className="text-right"><Trans ns="views/settings">users.table.actions</Trans></TableHead>
|
</TableHead>
|
||||||
|
<TableHead><Trans ns="views/settings">users.table.role</Trans></TableHead>
|
||||||
|
<TableHead className="text-right">
|
||||||
|
<Trans ns="views/settings">users.table.actions</Trans>
|
||||||
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -233,7 +261,7 @@ export default function AuthenticationView() {
|
|||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.role || "viewer"}
|
<Trans>role.{user.role || "viewer"}</Trans>
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
@ -262,7 +290,11 @@ export default function AuthenticationView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p><Trans ns="views/settings">users.table.changeRole</Trans></p>
|
<p>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.table.changeRole
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@ -280,12 +312,18 @@ export default function AuthenticationView() {
|
|||||||
>
|
>
|
||||||
<FaUserEdit className="size-3.5" />
|
<FaUserEdit className="size-3.5" />
|
||||||
<span className="ml-1.5 hidden sm:inline-block">
|
<span className="ml-1.5 hidden sm:inline-block">
|
||||||
<Trans ns="views/settings">users.table.password</Trans>
|
<Trans ns="views/settings">
|
||||||
|
users.table.password
|
||||||
|
</Trans>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p><Trans ns="views/settings">users.updatePassword</Trans></p>
|
<p>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.updatePassword
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
@ -308,7 +346,11 @@ export default function AuthenticationView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p><Trans ns="views/settings">users.table.deleteUser</Trans></p>
|
<p>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
users.table.deleteUser
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export default function CameraSettingsView({
|
|||||||
const alertsLabels = useMemo(() => {
|
const alertsLabels = useMemo(() => {
|
||||||
return cameraConfig?.review.alerts.labels
|
return cameraConfig?.review.alerts.labels
|
||||||
? cameraConfig.review.alerts.labels
|
? cameraConfig.review.alerts.labels
|
||||||
.map((label) => t(label, {ns: "objects"}))
|
.map((label) => t(label, { ns: "objects" }))
|
||||||
.join(", ")
|
.join(", ")
|
||||||
: "";
|
: "";
|
||||||
}, [cameraConfig]);
|
}, [cameraConfig]);
|
||||||
@ -86,7 +86,7 @@ export default function CameraSettingsView({
|
|||||||
const detectionsLabels = useMemo(() => {
|
const detectionsLabels = useMemo(() => {
|
||||||
return cameraConfig?.review.detections.labels
|
return cameraConfig?.review.detections.labels
|
||||||
? cameraConfig.review.detections.labels
|
? cameraConfig.review.detections.labels
|
||||||
.map((label) => t(label, {ns: "objects"}))
|
.map((label) => t(label, { ns: "objects" }))
|
||||||
.join(", ")
|
.join(", ")
|
||||||
: "";
|
: "";
|
||||||
}, [cameraConfig]);
|
}, [cameraConfig]);
|
||||||
@ -159,14 +159,9 @@ export default function CameraSettingsView({
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(
|
toast.success(t("camera.reviewClassification.toast.success"), {
|
||||||
t(
|
position: "top-center",
|
||||||
"camera.reviewClassification.toast.success",
|
});
|
||||||
),
|
|
||||||
{
|
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
updateConfig();
|
updateConfig();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -182,11 +177,14 @@ export default function CameraSettingsView({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("toast.save.error", {
|
toast.error(
|
||||||
errorMessage
|
t("toast.save.error", {
|
||||||
}), {
|
errorMessage,
|
||||||
position: "top-center",
|
}),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -322,9 +320,7 @@ export default function CameraSettingsView({
|
|||||||
/>
|
/>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="detections-enabled">
|
<Label htmlFor="detections-enabled">
|
||||||
<Trans ns="views/settings">
|
<Trans ns="views/settings">camera.review.detections</Trans>
|
||||||
camera.review.detections
|
|
||||||
</Trans>
|
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -465,19 +461,16 @@ export default function CameraSettingsView({
|
|||||||
cameraName: capitalizeFirstLetter(
|
cameraName: capitalizeFirstLetter(
|
||||||
cameraConfig?.name ?? "",
|
cameraConfig?.name ?? "",
|
||||||
).replaceAll("_", " "),
|
).replaceAll("_", " "),
|
||||||
ns: "views/settings"
|
ns: "views/settings",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: t(
|
: t("camera.reviewClassification.objectAlertsTips", {
|
||||||
"camera.reviewClassification.objectAlertsTips",
|
alertsLabels,
|
||||||
{
|
cameraName: capitalizeFirstLetter(
|
||||||
alertsLabels,
|
cameraConfig?.name ?? "",
|
||||||
cameraName: capitalizeFirstLetter(
|
).replaceAll("_", " "),
|
||||||
cameraConfig?.name ?? "",
|
ns: "views/settings",
|
||||||
).replaceAll("_", " "),
|
})}
|
||||||
ns: "views/settings"
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export default function MotionTunerView({
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success(
|
toast.success(
|
||||||
t("motionDetectionTuner.toast.success", { ns: "views/settings"}),
|
t("motionDetectionTuner.toast.success", { ns: "views/settings" }),
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
},
|
},
|
||||||
@ -128,12 +128,9 @@ export default function MotionTunerView({
|
|||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
updateConfig();
|
updateConfig();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
|
||||||
t("toast.save.error", { errorMessage: res.statusText }),
|
position: "top-center",
|
||||||
{
|
});
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -215,7 +212,9 @@ export default function MotionTunerView({
|
|||||||
<div className="mt-2 space-y-6">
|
<div className="mt-2 space-y-6">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="motion-threshold" className="text-md">
|
<Label htmlFor="motion-threshold" className="text-md">
|
||||||
<Trans ns="views/settings">motionDetectionTuner.Threshold</Trans>
|
<Trans ns="views/settings">
|
||||||
|
motionDetectionTuner.Threshold
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<div className="my-2 text-sm text-muted-foreground">
|
<div className="my-2 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
@ -246,7 +245,9 @@ export default function MotionTunerView({
|
|||||||
<div className="mt-2 space-y-6">
|
<div className="mt-2 space-y-6">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="motion-threshold" className="text-md">
|
<Label htmlFor="motion-threshold" className="text-md">
|
||||||
<Trans ns="views/settings">motionDetectionTuner.contourArea</Trans>
|
<Trans ns="views/settings">
|
||||||
|
motionDetectionTuner.contourArea
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
<div className="my-2 text-sm text-muted-foreground">
|
<div className="my-2 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -351,7 +351,9 @@ export default function NotificationView({
|
|||||||
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
<Trans ns="views/settings">notification.notificationSettings</Trans>
|
<Trans ns="views/settings">
|
||||||
|
notification.notificationSettings
|
||||||
|
</Trans>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
@ -366,7 +368,9 @@ export default function NotificationView({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline"
|
className="inline"
|
||||||
>
|
>
|
||||||
<Trans ns="views/settings">notification.documentation</Trans>{" "}
|
<Trans ns="views/settings">
|
||||||
|
notification.documentation
|
||||||
|
</Trans>{" "}
|
||||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -389,15 +393,16 @@ export default function NotificationView({
|
|||||||
<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] md:w-72"
|
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark] md:w-72"
|
||||||
placeholder={t(
|
placeholder={t("notification.email.placeholder", {
|
||||||
"notification.email.placeholder",
|
ns: "views/settings",
|
||||||
{ ns: "views/settings" }
|
})}
|
||||||
)}
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans ns="views/settings">notification.email.desc</Trans>
|
<Trans ns="views/settings">
|
||||||
|
notification.email.desc
|
||||||
|
</Trans>
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -424,7 +429,9 @@ export default function NotificationView({
|
|||||||
name="allEnabled"
|
name="allEnabled"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
label={t("cameras.all", { ns: "components/filter" })}
|
label={t("cameras.all", {
|
||||||
|
ns: "components/filter",
|
||||||
|
})}
|
||||||
isChecked={field.value}
|
isChecked={field.value}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setChangedValue(true);
|
setChangedValue(true);
|
||||||
@ -516,7 +523,9 @@ export default function NotificationView({
|
|||||||
<div className="flex flex-col gap-2 md:max-w-[50%]">
|
<div className="flex flex-col gap-2 md:max-w-[50%]">
|
||||||
<Separator className="my-2 flex bg-secondary md:hidden" />
|
<Separator className="my-2 flex bg-secondary md:hidden" />
|
||||||
<Heading as="h4" className="my-2">
|
<Heading as="h4" className="my-2">
|
||||||
<Trans ns="views/settings">notification.deviceSpecific</Trans>
|
<Trans ns="views/settings">
|
||||||
|
notification.deviceSpecific
|
||||||
|
</Trans>
|
||||||
</Heading>
|
</Heading>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Register or unregister notifications for this device"
|
aria-label="Register or unregister notifications for this device"
|
||||||
@ -560,8 +569,12 @@ export default function NotificationView({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{registration != null
|
{registration != null
|
||||||
? t("notification.unregisterDevice", { ns: "views/settings" })
|
? t("notification.unregisterDevice", {
|
||||||
: t("notification.registerDevice", { ns: "views/settings" })}
|
ns: "views/settings",
|
||||||
|
})
|
||||||
|
: t("notification.registerDevice", {
|
||||||
|
ns: "views/settings",
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
{registration != null && registration.active && (
|
{registration != null && registration.active && (
|
||||||
<Button
|
<Button
|
||||||
@ -656,7 +669,10 @@ export function CameraNotificationSwitch({
|
|||||||
time_style: "medium",
|
time_style: "medium",
|
||||||
date_style: "medium",
|
date_style: "medium",
|
||||||
timezone: config?.ui.timezone,
|
timezone: config?.ui.timezone,
|
||||||
strftime_fmt: config?.ui.time_format == "24hour" ? t("time.formattedTimestampExcludeSeconds.24hour"): t("time.formattedTimestampExcludeSeconds"),
|
strftime_fmt:
|
||||||
|
config?.ui.time_format == "24hour"
|
||||||
|
? t("time.formattedTimestampExcludeSeconds.24hour")
|
||||||
|
: t("time.formattedTimestampExcludeSeconds"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export default function ObjectSettingsView({
|
|||||||
.map((detector) => capitalizeFirstLetter(detector))
|
.map((detector) => capitalizeFirstLetter(detector))
|
||||||
.join(",")
|
.join(",")
|
||||||
: "",
|
: "",
|
||||||
ns: "views/settings",
|
ns: "views/settings",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@ -264,7 +264,9 @@ export default function ObjectSettingsView({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div className="cursor-pointer p-0">
|
<div className="cursor-pointer p-0">
|
||||||
<LuInfo className="size-4" />
|
<LuInfo className="size-4" />
|
||||||
<span className="sr-only"><Trans>button.info</Trans></span>
|
<span className="sr-only">
|
||||||
|
<Trans>button.info</Trans>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-80 text-sm">
|
<PopoverContent className="w-80 text-sm">
|
||||||
|
|||||||
@ -100,12 +100,9 @@ export default function ExploreSettingsView({
|
|||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
updateConfig();
|
updateConfig();
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(t("toast.save.error", { errorMessage: res.statusText }), {
|
||||||
t("toast.save.error", { errorMessage: res.statusText }),
|
position: "top-center",
|
||||||
{
|
});
|
||||||
position: "top-center",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -113,11 +110,14 @@ export default function ExploreSettingsView({
|
|||||||
error.response?.data?.message ||
|
error.response?.data?.message ||
|
||||||
error.response?.data?.detail ||
|
error.response?.data?.detail ||
|
||||||
"Unknown error";
|
"Unknown error";
|
||||||
toast.error(t("toast.save.error", {
|
toast.error(
|
||||||
errorMessage
|
t("toast.save.error", {
|
||||||
}), {
|
errorMessage,
|
||||||
position: "top-center",
|
}),
|
||||||
});
|
{
|
||||||
|
position: "top-center",
|
||||||
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -272,7 +272,7 @@ export default function ExploreSettingsView({
|
|||||||
{t(
|
{t(
|
||||||
"explore.semanticSearch.modelSize." +
|
"explore.semanticSearch.modelSize." +
|
||||||
ExploreSettings.model_size,
|
ExploreSettings.model_size,
|
||||||
{ ns: "views/settings"}
|
{ ns: "views/settings" },
|
||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -283,11 +283,9 @@ export default function ExploreSettingsView({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
value={size}
|
value={size}
|
||||||
>
|
>
|
||||||
{t(
|
{t("explore.semanticSearch.modelSize." + size, {
|
||||||
"explore.semanticSearch.modelSize." +
|
ns: "views/settings",
|
||||||
size,
|
})}
|
||||||
{ ns: "views/settings"}
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@ -129,7 +129,9 @@ export default function UiSettingsView() {
|
|||||||
onCheckedChange={setAlertVideos}
|
onCheckedChange={setAlertVideos}
|
||||||
/>
|
/>
|
||||||
<Label className="cursor-pointer" htmlFor="images-only">
|
<Label className="cursor-pointer" htmlFor="images-only">
|
||||||
<Trans ns="views/settings">general.liveDashboard.playAlertVideos.label</Trans>
|
<Trans ns="views/settings">
|
||||||
|
general.liveDashboard.playAlertVideos.label
|
||||||
|
</Trans>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||||
@ -264,7 +266,7 @@ export default function UiSettingsView() {
|
|||||||
{t(
|
{t(
|
||||||
"general.calendar.firstWeekday." +
|
"general.calendar.firstWeekday." +
|
||||||
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
|
WEEK_STARTS_ON[weekStartsOn ?? 0].toLowerCase(),
|
||||||
{ns: "views/settings"}
|
{ ns: "views/settings" },
|
||||||
)}
|
)}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -275,11 +277,9 @@ export default function UiSettingsView() {
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
value={index.toString()}
|
value={index.toString()}
|
||||||
>
|
>
|
||||||
{t(
|
{t("general.calendar.firstWeekday." + day.toLowerCase(), {
|
||||||
"general.calendar.firstWeekday." +
|
ns: "views/settings",
|
||||||
day.toLowerCase(),
|
})}
|
||||||
{ns: "views/settings"}
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
|
|||||||
@ -300,7 +300,9 @@ export default function CameraMetrics({
|
|||||||
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
{Object.keys(cameraFpsSeries).includes(camera.name) ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/system">cameras.framesAndDetections</Trans>
|
<Trans ns="views/system">
|
||||||
|
cameras.framesAndDetections
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<CameraLineGraph
|
<CameraLineGraph
|
||||||
graphId={`${camera.name}-dps`}
|
graphId={`${camera.name}-dps`}
|
||||||
|
|||||||
@ -449,7 +449,7 @@ export default function GeneralMetrics({
|
|||||||
|
|
||||||
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
|
||||||
<div className="text-sm font-medium text-muted-foreground">
|
<div className="text-sm font-medium text-muted-foreground">
|
||||||
<Trans ns="views/settings">general.detector</Trans>
|
<Trans ns="views/system">general.detector.title</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -460,7 +460,7 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.detectorInferenceSpeed</Trans>
|
<Trans ns="views/system">general.detector.inferenceSpeed</Trans>
|
||||||
</div>
|
</div>
|
||||||
{detInferenceTimeSeries.map((series) => (
|
{detInferenceTimeSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -500,7 +500,7 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.detectorCpuUsage</Trans>
|
<Trans ns="views/system">general.detector.cpuUsage</Trans>
|
||||||
</div>
|
</div>
|
||||||
{detCpuSeries.map((series) => (
|
{detCpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -520,7 +520,7 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.detectorMemoryUsage</Trans>
|
<Trans ns="views/system">general.detector.memoryUsage</Trans>
|
||||||
</div>
|
</div>
|
||||||
{detMemSeries.map((series) => (
|
{detMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -552,7 +552,7 @@ export default function GeneralMetrics({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowVainfo(true)}
|
onClick={() => setShowVainfo(true)}
|
||||||
>
|
>
|
||||||
<Trans ns="views/settings">general.hardwareInfo</Trans>
|
<Trans ns="views/system">general.hardwareInfo.title</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -565,7 +565,9 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.gpuUsage</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.hardwareInfo.gpuUsage
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{gpuSeries.map((series) => (
|
{gpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -587,7 +589,9 @@ export default function GeneralMetrics({
|
|||||||
{gpuMemSeries && (
|
{gpuMemSeries && (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.gpuMemroy</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.hardwareInfo.gpuMemroy
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{gpuMemSeries.map((series) => (
|
{gpuMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -611,7 +615,9 @@ export default function GeneralMetrics({
|
|||||||
{gpuEncSeries && gpuEncSeries?.length != 0 && (
|
{gpuEncSeries && gpuEncSeries?.length != 0 && (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.gpuEncoder</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.hardwareInfo.gpuEncoder
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{gpuEncSeries.map((series) => (
|
{gpuEncSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -635,7 +641,9 @@ export default function GeneralMetrics({
|
|||||||
{gpuDecSeries && gpuDecSeries?.length != 0 && (
|
{gpuDecSeries && gpuDecSeries?.length != 0 && (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.gpuDecoder</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.hardwareInfo.gpuDecoder
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{gpuDecSeries.map((series) => (
|
{gpuDecSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -659,13 +667,15 @@ export default function GeneralMetrics({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
<div className="mt-4 text-sm font-medium text-muted-foreground">
|
||||||
<Trans ns="views/settings">general.otherProcesses</Trans>
|
<Trans ns="views/system">general.otherProcesses.title</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.processCpuUsage</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.otherProcesses.processCpuUsage
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{otherProcessCpuSeries.map((series) => (
|
{otherProcessCpuSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
@ -685,7 +695,9 @@ export default function GeneralMetrics({
|
|||||||
{statsHistory.length != 0 ? (
|
{statsHistory.length != 0 ? (
|
||||||
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Trans ns="views/settings">general.processMemoryUsage</Trans>
|
<Trans ns="views/system">
|
||||||
|
general.otherProcesses.processMemoryUsage
|
||||||
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
{otherProcessMemSeries.map((series) => (
|
{otherProcessMemSeries.map((series) => (
|
||||||
<ThresholdBarGraph
|
<ThresholdBarGraph
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user