@@ -16,6 +16,7 @@ import { formatFilenameTimestamp } from '../../utils/date-utils.js';
1616import { forceNavigation } from '../../utils/navigation-utils.js' ;
1717import { formatUtils } from './recordings/formatUtils.js' ;
1818import { useI18n } from '../../i18n.js' ;
19+ import { useQueryClient } from '../../query-client.js' ;
1920import 'webrtc-adapter' ;
2021
2122// Retry configuration for sending WebRTC offers to go2rtc.
@@ -67,6 +68,8 @@ export function WebRTCVideoCell({
6768 globalShowDetections = true
6869} ) {
6970 const { t } = useI18n ( ) ;
71+ const queryClient = useQueryClient ( ) ;
72+
7073 // Component state
7174 const [ isLoading , setIsLoading ] = useState ( ( ) => {
7275 // Derive initial loading state from the incoming stream, so that we
@@ -125,6 +128,11 @@ export function WebRTCVideoCell({
125128 // PTZ controls state
126129 const [ showPTZControls , setShowPTZControls ] = useState ( false ) ;
127130
131+ // Disable/enable stream state
132+ const [ showDisableConfirm , setShowDisableConfirm ] = useState ( false ) ;
133+ const [ localIsDisabled , setLocalIsDisabled ] = useState ( false ) ;
134+ const [ isTogglingEnabled , setIsTogglingEnabled ] = useState ( false ) ;
135+
128136 // Detection overlay visibility state (per-camera toggle, constrained by global toggle)
129137 const [ localShowDetections , setLocalShowDetections ] = useState ( true ) ;
130138 const showDetections = globalShowDetections && localShowDetections ;
@@ -933,6 +941,46 @@ export function WebRTCVideoCell({
933941 setRetryCount ( prev => prev + 1 ) ;
934942 } ;
935943
944+ /**
945+ * Handle disable stream (soft delete)
946+ */
947+ const handleDisableStream = async ( ) => {
948+ setIsTogglingEnabled ( true ) ;
949+ try {
950+ const res = await fetch ( `/api/streams/${ encodeURIComponent ( stream . name ) } ` , { method : 'DELETE' } ) ;
951+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
952+ setLocalIsDisabled ( true ) ;
953+ setShowDisableConfirm ( false ) ;
954+ queryClient . invalidateQueries ( { queryKey : [ 'streams' ] } ) ;
955+ } catch ( err ) {
956+ showStatusMessage ( `${ t ( 'live.disableStream' ) } : ${ err . message } ` , 'error' , 5000 ) ;
957+ setShowDisableConfirm ( false ) ;
958+ } finally {
959+ setIsTogglingEnabled ( false ) ;
960+ }
961+ } ;
962+
963+ /**
964+ * Handle enable stream
965+ */
966+ const handleEnableStream = async ( ) => {
967+ setIsTogglingEnabled ( true ) ;
968+ try {
969+ const res = await fetch ( `/api/streams/${ encodeURIComponent ( stream . name ) } ` , {
970+ method : 'PUT' ,
971+ headers : { 'Content-Type' : 'application/json' } ,
972+ body : JSON . stringify ( { enable_disabled : true , enabled : true } )
973+ } ) ;
974+ if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` ) ;
975+ setLocalIsDisabled ( false ) ;
976+ queryClient . invalidateQueries ( { queryKey : [ 'streams' ] } ) ;
977+ } catch ( err ) {
978+ showStatusMessage ( `${ t ( 'live.enableStream' ) } : ${ err . message } ` , 'error' , 5000 ) ;
979+ } finally {
980+ setIsTogglingEnabled ( false ) ;
981+ }
982+ } ;
983+
936984 // Start audio level monitoring
937985 const startAudioLevelMonitoring = useCallback ( ( localStream ) => {
938986 try {
@@ -1249,6 +1297,31 @@ export function WebRTCVideoCell({
12491297 } }
12501298 />
12511299 </ div >
1300+ { /* Disable stream button */ }
1301+ < button
1302+ type = "button"
1303+ title = { t ( 'live.disableStream' ) }
1304+ onClick = { ( ) => setShowDisableConfirm ( true ) }
1305+ style = { {
1306+ backgroundColor : 'transparent' ,
1307+ border : 'none' ,
1308+ padding : '5px' ,
1309+ borderRadius : '4px' ,
1310+ color : 'white' ,
1311+ cursor : 'pointer' ,
1312+ transition : 'background-color 0.2s ease' ,
1313+ display : 'flex' ,
1314+ alignItems : 'center' ,
1315+ justifyContent : 'center'
1316+ } }
1317+ onMouseOver = { ( e ) => e . currentTarget . style . backgroundColor = 'rgba(255, 255, 255, 0.2)' }
1318+ onMouseOut = { ( e ) => e . currentTarget . style . backgroundColor = 'transparent' }
1319+ >
1320+ < svg xmlns = "http://www.w3.org/2000/svg" width = "24" height = "24" viewBox = "0 0 24 24" fill = "none" stroke = "white" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
1321+ < path d = "M18.36 6.64A9 9 0 1 1 5.64 17.36" />
1322+ < line x1 = "12" y1 = "2" x2 = "12" y2 = "12" />
1323+ </ svg >
1324+ </ button >
12521325 { /* Audio playback toggle button (for hearing camera audio) */ }
12531326 { isPlaying && (
12541327 < button
@@ -1630,6 +1703,68 @@ export function WebRTCVideoCell({
16301703 message = { t ( 'live.forceRefreshWarning' ) }
16311704 confirmLabel = { t ( 'common.refresh' ) }
16321705 />
1706+
1707+ { /* Inline disable confirmation overlay */ }
1708+ { showDisableConfirm && (
1709+ < div style = { {
1710+ position : 'absolute' , top : 0 , left : 0 , right : 0 , bottom : 0 ,
1711+ backgroundColor : 'rgba(0,0,0,0.75)' , zIndex : 20 ,
1712+ display : 'flex' , flexDirection : 'column' ,
1713+ alignItems : 'center' , justifyContent : 'center' , gap : '12px' ,
1714+ padding : '16px' , textAlign : 'center'
1715+ } } >
1716+ < p style = { { color : 'white' , fontSize : '14px' , maxWidth : '240px' , lineHeight : '1.4' } } >
1717+ { t ( 'live.disableStreamConfirm' ) }
1718+ </ p >
1719+ < div style = { { display : 'flex' , gap : '8px' } } >
1720+ < button
1721+ onClick = { handleDisableStream }
1722+ disabled = { isTogglingEnabled }
1723+ style = { {
1724+ padding : '6px 16px' , backgroundColor : '#dc2626' , color : 'white' ,
1725+ border : 'none' , borderRadius : '4px' , cursor : 'pointer' , fontWeight : 'bold' , fontSize : '13px'
1726+ } }
1727+ >
1728+ { t ( 'live.disableStream' ) }
1729+ </ button >
1730+ < button
1731+ onClick = { ( ) => setShowDisableConfirm ( false ) }
1732+ style = { {
1733+ padding : '6px 16px' , backgroundColor : 'rgba(255,255,255,0.2)' , color : 'white' ,
1734+ border : '1px solid rgba(255,255,255,0.4)' , borderRadius : '4px' , cursor : 'pointer' , fontSize : '13px'
1735+ } }
1736+ >
1737+ { t ( 'common.cancel' ) }
1738+ </ button >
1739+ </ div >
1740+ </ div >
1741+ ) }
1742+
1743+ { /* Locally disabled overlay */ }
1744+ { localIsDisabled && (
1745+ < div style = { {
1746+ position : 'absolute' , top : 0 , left : 0 , right : 0 , bottom : 0 ,
1747+ backgroundColor : 'rgba(0,0,0,0.85)' , zIndex : 15 ,
1748+ display : 'flex' , flexDirection : 'column' ,
1749+ alignItems : 'center' , justifyContent : 'center' , gap : '12px'
1750+ } } >
1751+ < svg xmlns = "http://www.w3.org/2000/svg" width = "40" height = "40" viewBox = "0 0 24 24" fill = "none" stroke = "rgba(255,255,255,0.5)" strokeWidth = "1.5" strokeLinecap = "round" strokeLinejoin = "round" >
1752+ < path d = "M18.36 6.64A9 9 0 1 1 5.64 17.36" />
1753+ < line x1 = "12" y1 = "2" x2 = "12" y2 = "12" />
1754+ </ svg >
1755+ < p style = { { color : 'rgba(255,255,255,0.7)' , fontSize : '14px' } } > { t ( 'live.streamDisabled' ) } </ p >
1756+ < button
1757+ onClick = { handleEnableStream }
1758+ disabled = { isTogglingEnabled }
1759+ style = { {
1760+ padding : '6px 16px' , backgroundColor : '#16a34a' , color : 'white' ,
1761+ border : 'none' , borderRadius : '4px' , cursor : 'pointer' , fontWeight : 'bold' , fontSize : '13px'
1762+ } }
1763+ >
1764+ { t ( 'live.enableStream' ) }
1765+ </ button >
1766+ </ div >
1767+ ) }
16331768 </ div >
16341769 ) ;
16351770}
0 commit comments