Commit 9e0b7420 authored by Shehan Liyanage's avatar Shehan Liyanage

add changes

parent 8fd395e6
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"agora-rtc-react": "2.0.0-beta.0", "agora-rtc-react": "2.0.0-beta.0",
"agora-rtc-sdk-ng": "4.18.2", "agora-rtc-sdk-ng": "4.18.2",
"axios": "^1.6.8",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.1",
...@@ -5934,6 +5935,29 @@ ...@@ -5934,6 +5935,29 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
...@@ -9662,9 +9686,9 @@ ...@@ -9662,9 +9686,9 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.5", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
...@@ -16244,6 +16268,11 @@ ...@@ -16244,6 +16268,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/psl": { "node_modules/psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
...@@ -24257,6 +24286,28 @@ ...@@ -24257,6 +24286,28 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
"integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==" "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ=="
}, },
"axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"axobject-query": { "axobject-query": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
...@@ -26931,9 +26982,9 @@ ...@@ -26931,9 +26982,9 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.15.5", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
}, },
"for-each": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
...@@ -31546,6 +31597,11 @@ ...@@ -31546,6 +31597,11 @@
} }
} }
}, },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"psl": { "psl": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
...@@ -20,8 +20,8 @@ function Copyright(props: any) { ...@@ -20,8 +20,8 @@ function Copyright(props: any) {
return ( return (
<Typography variant='body2' color='text.secondary' align='center' {...props}> <Typography variant='body2' color='text.secondary' align='center' {...props}>
{'Copyright © '} {'Copyright © '}
<Link color='inherit' href='https://mui.com/'> <Link color='inherit' href='http://localhost:3000/SignInSide#'>
Your Website SPEAKEZY
</Link>{' '} </Link>{' '}
{new Date().getFullYear()} {new Date().getFullYear()}
{'.'} {'.'}
...@@ -33,18 +33,43 @@ const defaultTheme = createTheme() ...@@ -33,18 +33,43 @@ const defaultTheme = createTheme()
export default function SignInSide() { export default function SignInSide() {
const navigate = useNavigate() const navigate = useNavigate()
const [emailError, setEmailError] = React.useState<string | null>(null)
const [passwordError, setPasswordError] = React.useState<string | null>(null)
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
const data = new FormData(event.currentTarget) const data = new FormData(event.currentTarget)
console.log({ const email = data.get('email') as string
email: data.get('email'), const password = data.get('password') as string
password: data.get('password'),
})
// Simulate authentication logic here let valid = true
// If authentication is successful, navigate to VideoCall page
navigate('/videocall') // Email validation using regular expression
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email || !emailRegex.test(email)) {
setEmailError('Please enter a valid email address')
valid = false
} else {
setEmailError(null)
}
if (!password || password.length < 6) {
setPasswordError('Password must be at least 6 characters long')
valid = false
} else {
setPasswordError(null)
}
if (valid) {
console.log({
email: email,
password: password,
})
// Simulate authentication logic here
// If authentication is successful, navigate to VideoCall page
navigate('/videocall')
}
} }
return ( return (
...@@ -91,6 +116,8 @@ export default function SignInSide() { ...@@ -91,6 +116,8 @@ export default function SignInSide() {
name='email' name='email'
autoComplete='email' autoComplete='email'
autoFocus autoFocus
error={!!emailError}
helperText={emailError}
/> />
<TextField <TextField
margin='normal' margin='normal'
...@@ -101,6 +128,8 @@ export default function SignInSide() { ...@@ -101,6 +128,8 @@ export default function SignInSide() {
type='password' type='password'
id='password' id='password'
autoComplete='current-password' autoComplete='current-password'
error={!!passwordError}
helperText={passwordError}
/> />
<FormControlLabel <FormControlLabel
control={<Checkbox value='remember' color='primary' />} control={<Checkbox value='remember' color='primary' />}
......
...@@ -13,9 +13,7 @@ const VideoCall = () => { ...@@ -13,9 +13,7 @@ const VideoCall = () => {
<Box sx={{ height: '100vh', width: '100vw', overflow: 'hidden' }}> <Box sx={{ height: '100vh', width: '100vw', overflow: 'hidden' }}>
{joined ? ( {joined ? (
<AgoraRTCProvider client={agoraEngine}> <AgoraRTCProvider client={agoraEngine}>
<AgoraManager setJoined={setJoined}> <AgoraManager setJoined={setJoined} />
<div>asas</div>
</AgoraManager>
</AgoraRTCProvider> </AgoraRTCProvider>
) : ( ) : (
<Lobby setJoined={setJoined} /> <Lobby setJoined={setJoined} />
......
import React, { createContext, useContext, useEffect } from 'react' import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
import { useJoin, useLocalCameraTrack, useLocalMicrophoneTrack } from 'agora-rtc-react' import { useJoin, useLocalCameraTrack, useLocalMicrophoneTrack } from 'agora-rtc-react'
import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng' import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import { Box } from '@mui/material' import { Box, Tabs, Tab, Typography } from '@mui/material'
import { useTheme, type Theme } from '@mui/material/styles' import { useTheme, type Theme } from '@mui/material/styles'
import MeetingLoading from '../../Components/MeetingLoading/MeetingLoading' import MeetingLoading from '../../Components/MeetingLoading/MeetingLoading'
import MeetingLayout from '../../Components/MeetingLayout/MeetingLayout' import MeetingLayout from '../../Components/MeetingLayout/MeetingLayout'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import axios from 'axios'
interface AgoraContextType { interface AgoraContextType {
localCameraTrack: ICameraVideoTrack | null localCameraTrack: ICameraVideoTrack | null
localMicrophoneTrack: IMicrophoneAudioTrack | null localMicrophoneTrack: IMicrophoneAudioTrack | null
children: React.ReactNode children?: React.ReactNode
} }
const AgoraContext = createContext<AgoraContextType | null>(null) const AgoraContext = createContext<AgoraContextType | null>(null)
...@@ -20,7 +21,7 @@ export const AgoraProvider: React.FC<AgoraContextType> = ({ ...@@ -20,7 +21,7 @@ export const AgoraProvider: React.FC<AgoraContextType> = ({
localCameraTrack, localCameraTrack,
localMicrophoneTrack, localMicrophoneTrack,
}) => ( }) => (
<AgoraContext.Provider value={{ localCameraTrack, localMicrophoneTrack, children }}> <AgoraContext.Provider value={{ localCameraTrack, localMicrophoneTrack }}>
{children} {children}
</AgoraContext.Provider> </AgoraContext.Provider>
) )
...@@ -31,14 +32,11 @@ export const useAgoraContext = () => { ...@@ -31,14 +32,11 @@ export const useAgoraContext = () => {
return context return context
} }
export const AgoraManager = ({ export const AgoraManager = ({ setJoined }: { setJoined: (x: boolean) => void }) => {
setJoined,
children,
}: {
setJoined: (x: boolean) => void
children: React.ReactNode
}) => {
const theme: Theme = useTheme() const theme: Theme = useTheme()
const videoRef = useRef<HTMLVideoElement | null>(null)
const [predictions, setPredictions] = useState<string[]>([])
const [tabValue, setTabValue] = useState(0)
let { channel } = useParams<{ channel: string }>() let { channel } = useParams<{ channel: string }>()
if (!channel) { if (!channel) {
channel = Array.from({ length: 8 }, () => Math.random().toString(36)[2] || 0).join('') channel = Array.from({ length: 8 }, () => Math.random().toString(36)[2] || 0).join('')
...@@ -69,6 +67,45 @@ export const AgoraManager = ({ ...@@ -69,6 +67,45 @@ export const AgoraManager = ({
} }
}, []) }, [])
useEffect(() => {
const captureFrame = async () => {
if (videoRef.current) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (context) {
canvas.width = videoRef.current.videoWidth
canvas.height = videoRef.current.videoHeight
context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height)
const base64Frame = canvas.toDataURL('image/jpeg').split(',')[1]
try {
const response = await axios.post('http://localhost:8000/endpoint', {
frame: base64Frame,
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setPredictions(response.data.sentence)
} catch (error) {
console.error('Error sending frame to backend:', error)
}
}
}
}
const interval = setInterval(captureFrame, 1000) // Adjust the interval as needed (e.g., 1000ms for 1 second)
return () => clearInterval(interval)
}, [localCameraTrack])
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue)
}
useEffect(() => {
if (localCameraTrack && videoRef.current) {
localCameraTrack.play(videoRef.current)
}
}, [localCameraTrack])
return ( return (
<Box <Box
sx={{ sx={{
...@@ -76,6 +113,7 @@ export const AgoraManager = ({ ...@@ -76,6 +113,7 @@ export const AgoraManager = ({
height: '100%', height: '100%',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
display: 'flex', display: 'flex',
flexDirection: 'column',
}} }}
> >
{isJoining ? ( {isJoining ? (
...@@ -87,11 +125,28 @@ export const AgoraManager = ({ ...@@ -87,11 +125,28 @@ export const AgoraManager = ({
localCameraTrack={localCameraTrack} localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack} localMicrophoneTrack={localMicrophoneTrack}
> >
<MeetingLayout <Tabs value={tabValue} onChange={handleTabChange} aria-label='simple tabs example'>
setJoined={setJoined} <Tab label='Meeting' />
localCameraTrack={localCameraTrack} <Tab label='CC' />
localMicrophoneTrack={localMicrophoneTrack} </Tabs>
/> {tabValue === 0 && (
<>
<MeetingLayout
setJoined={setJoined}
localCameraTrack={localCameraTrack}
localMicrophoneTrack={localMicrophoneTrack}
/>
<video ref={videoRef} style={{ display: 'none' }} />
</>
)}
{tabValue === 1 && (
<Box sx={{ p: 3 }}>
<Typography variant='h6'>Predicted Actions:</Typography>
{predictions.map((prediction, index) => (
<Typography key={index}>{prediction}</Typography>
))}
</Box>
)}
</AgoraProvider> </AgoraProvider>
) : joinError ? ( ) : joinError ? (
<MeetingLoading variant='error' /> <MeetingLoading variant='error' />
......
...@@ -3223,6 +3223,15 @@ ...@@ -3223,6 +3223,15 @@
"resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" "resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz"
"version" "4.7.0" "version" "4.7.0"
"axios@^1.6.8":
"integrity" "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ=="
"resolved" "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz"
"version" "1.6.8"
dependencies:
"follow-redirects" "^1.15.6"
"form-data" "^4.0.0"
"proxy-from-env" "^1.1.0"
"axobject-query@^3.2.1": "axobject-query@^3.2.1":
"integrity" "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==" "integrity" "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg=="
"resolved" "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz" "resolved" "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz"
...@@ -5284,10 +5293,10 @@ ...@@ -5284,10 +5293,10 @@
"resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz" "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz"
"version" "3.2.9" "version" "3.2.9"
"follow-redirects@^1.0.0": "follow-redirects@^1.0.0", "follow-redirects@^1.15.6":
"integrity" "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" "integrity" "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
"resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz" "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz"
"version" "1.15.5" "version" "1.15.6"
"for-each@^0.3.3": "for-each@^0.3.3":
"integrity" "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==" "integrity" "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw=="
...@@ -5332,6 +5341,15 @@ ...@@ -5332,6 +5341,15 @@
"combined-stream" "^1.0.8" "combined-stream" "^1.0.8"
"mime-types" "^2.1.12" "mime-types" "^2.1.12"
"form-data@^4.0.0":
"integrity" "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww=="
"resolved" "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
"version" "4.0.0"
dependencies:
"asynckit" "^0.4.0"
"combined-stream" "^1.0.8"
"mime-types" "^2.1.12"
"forwarded@0.2.0": "forwarded@0.2.0":
"integrity" "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" "integrity" "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
"resolved" "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" "resolved" "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
...@@ -8520,6 +8538,11 @@ ...@@ -8520,6 +8538,11 @@
"forwarded" "0.2.0" "forwarded" "0.2.0"
"ipaddr.js" "1.9.1" "ipaddr.js" "1.9.1"
"proxy-from-env@^1.1.0":
"integrity" "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"resolved" "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
"version" "1.1.0"
"psl@^1.1.33": "psl@^1.1.33":
"integrity" "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" "integrity" "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
"resolved" "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" "resolved" "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment